给新手(笨笨)准备的Flux介绍

原文地址:http://blog.andrewray.me/flux-for-stupid-people/

篇幅过长,没有阅读(TL;DR)。作为一个脑袋不咋灵光的人,在我挣扎着学习Flux时,希望有人能告诉我这篇文章里的内容。这不是一篇简单的、好的文档,而且它还有许多需要改正的部分内容。

这篇文章,紧接着上篇《给新手(笨笨)准备的ReactJS介绍》

我该用Flux么?

如果你的应用要处理一些动态数据的话,那么答案是“是的”,你很可能应该用Flux。

如果你的应用只是不用共享状态的静态页面,且你永远不需要保存或者更新数据,那么答案是“”,Flux不会给你带来任何提升。

为什么用Flux?

小幽默下,因为Flux本是个难以理解的概念,为什么你还把问题整的更加复杂?

90%的iOS应用通过table view呈现数据。iOS工具包把数据呈现与数据模型定义的非常好,以至于让开发过程变得很简单。

在前端,我们没有如iOS那样的便利条件,相反的我们有个大麻烦,那就是没人知道怎么正确的构建一个前端应用程序。说真的,我在这个行业工作了几年了,从没有一个“最佳实践”讲授这方面内容的。相对应的“库”倒是很多,jQuery?Angular?Backbone?Handlebars?这些库都避开了数据流(data flow)的处理,数据流该怎么处理始终困扰着我们。

Flux是啥?

Flux是一个描述了对指定的事件和监听器进行“单向”数据流处理的一个概念词。所以根本没有名叫Flux的这么个库,但你需要的是Flux分配器(Flux Dispatcher)和任何一个Javascript事件库

官方文档被写得很意识流(stream of conciousness),而且它并不适合开始学习Flux。一旦你理解了Flux,再去看文档会感觉融会贯通。

不要尝试去比较Flux和MVC架构,那会让你更加困惑。

好了,让我们开始吧,为了解释Flux的概念我会用例子带你了解它们。

1. 视图“分派了”“一个操作”

一个分配器(dispatcher)是基于一个事件系统使用的。它被用来广播触发的事件并注册事件的回调,而且有且只有一个全局分配器。你应该使用Facebook Dispatcher Library。它非常容易初始化:

1
var AppDispatcher = new Dispatcher();

让我们做个“new”按钮,用来像列表中添加个事项。

1
<button onClick={ this.createNewItem }>New Item</button>

在按钮被点击时发生了什么?你的视图派发了一个特定的事件,事件中包含着事件名和新事项的数据,如下代码:

1
2
3
4
5
6
createNewItem: function( evt ) {
AppDispatcher.dispatch({
eventName: 'new-item',
newItem: { name: 'Marco' } // example data
});
}

2. “存储”响应了分配器事件

跟Flux一样,“Store”(存储)也是Facebook发明的一个词。在我们的应用程序中,我们需要为列表指定一个集合来存放逻辑和数据。这就是我们的“Store”,我们暂且称之为ListStore。

一个store作为一个单例模型,这意味着你不应该使用new来声明它。ListStore是一个全局的对象,如下:

1
2
3
4
5
6
7
8
9
10
11
// 全局的对象描述了数据和逻辑
var ListStore = {

// 模型数据的真实数据集合
items: [],

// 附加的方法,我们稍后会用到
getAll: function() {
return this.items;
}
};

你的store接下来响应了被分配的事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var ListStore = …

AppDispatcher.register( function( payload ) {

switch( payload.eventName ) {

case 'new-item':

// 我们得到了变更后(mutate)的数据!
ListStore.items.push( payload.newItem );
break;

}

return true; // 为了实现Flux的Promise必须加的

});

这就是Flux如何派发回调函数的传统方式。每个payload包含了一个事件名和数据。一个switch分支条件决定具体用什么操作。

🔑 关键概念:Store并不是模型(model),一个store包含一个或多个模型(models)

🔑 关键概念:应用中的每个store只专注于如何更新数据。这是Flux中最关键的部分。我们派发的事件并不知道如何添加或删除数据元素。

如果,举个例子,你程序中另一部分需要保存一些图片和它们的元数据,你应再新建个store,起名为ImageStore。在程序中,每个store代表一个单独的“领土”(domain)。如果你的程序比较大,领土(domain)需要命名的很容易识别,如果你的程序较小,很可能你只需要1个store。

只有你的store们被允许注册到分配器的回调上。你的视图不应该调用AppDispatcher.register。分配器只用来从视图向store们发送消息。视图将响应不同于事件的类型。

3. Store触发了一个change事件

我们快接近核心了!现在数据确实改变了,我们要通知全世界。

你的store触发一个事件,但不使用分配器。这有点令人困惑,但这就是Flux的特殊方式。让我们给我们的store一个触发事件的能力。如果你正在用MicroEvent.js,那么是非常简单的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//MicroEvent.mixin( ListStore );  

//Then let's trigger our change event:

// 让我们触发change事件:

case 'new-item':

ListStore.items.push( payload.newItem );

// 向世界大声喊着“我们改变了!”
ListStore.trigger( 'change' );

break;

🔑 关键概念:在触发change事件时,不用传最新的item值,视图只关心什么东西发生了改变。让我们继续跟踪数据,去理解到底是为什么。

4. Event视图响应了change事件

现在我们需要显示列表。列表内容改变时,我们的视图要完全的重新渲染。那不是个错误。

首先,我们让组件首次被创建时监听从ListStore发来的change事件:

1
2
3
componentDidMount: function() {  
ListStore.bind( 'change', this.listChanged );
},

For simplicity’s sake, we’ll just call forceUpdate, which triggers a re-render. A different approach would be to store the entire list in state.

为了简单起见,我们只调用forceUpdate方法,它触发了一个重绘操作,这是个不同的处理方法,把整个列表存储到状态(state)中。

1
2
3
4
listChanged: function() {  
// Since the list changed, trigger a new render.
this.forceUpdate();
},

Don’t forget to clean up your event listeners when your component “unmounts,” which is when it goes back to hell:

在你的组件卸载时(unmounts),不要忘记清理事件监听:

1
2
3
componentWillUnmount: function() {  
ListStore.unbind( 'change', this.listChanged );
},

Now what? Let’s look at our render function, which I’ve purposely saved for last.

现在呢?让我们看下我们的渲染函数,这是整个函数的全貌:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
render: function() {

// 切记,ListStore是全局的
var items = ListStore.getAll();

// 在整个列表上使用循环创建列表项
var itemHtml = items.map( function( item ) {

// "key"很重要,应该是唯一的,用来标记每个列表项
return <li key={ listItem.id }>
{ listItem.name }
</li>;

});

return <div>
<ul>
{ itemHtml }
</ul>

<button onClick={ this.createNewItem }>New Item</button>

</div>;
}

我们回顾下整个流程。当你为列表新添加一项时,view分派一个动作(action),store响应这个动作(action),store随之更新,同时触发一个change事件,view响应这个change事件进行重绘。

但还有个问题,当列表改变时我们重绘整个view,这是不是效率太低了?!

当然不是。

诚然,我们再一次的调用重绘方法,自然的所有渲染方法的代码都要重新运行。但React将只重绘你修改了的真正DOMrender函数实际上生成了一个”virtual DOM”,React会对比上一次render的输出,如果两个virtual DOM不同,React只会更新他们之间的不同DOM点而不是整个DOM。

🔑 关键概念:当store数据改变时, view不应该关心数据是被新添、删除或者是被修改。 它们(数据)应完全的重绘。React的“virtual DOM”的diff算法承担了“搞清楚真正的DOM节点变化”的重任。这样让开发者更轻松(原句:让你的生活更简单,避免你的血压升高)。

还有一件事:Action Creator是个啥玩意?

记住,当我们点击按钮,我们分派了一个特定的事件:

1
2
3
4
AppDispatcher.dispatch({  
eventName: 'new-item',
newItem: { name: 'Samantha' }
});

如果你的许多视图都要需要触发这个事件,那么要重复书写非常多的代码。再加上,你所有的视图需要知道特定的对象格式,这样是很别扭的。所以Flux建议一个抽象,叫做 action creators(操作生成器),它只是把上面的代码抽象为一个方法。

1
2
3
4
5
6
7
8
ListActions = {
add: function( item ) {
AppDispatcher.dispatch({
eventName: 'new-item',
newItem: item
});
}
};

现在视图只要简单的调用ListActions.add({ name: '...' });并且不用担心被分配的对象语法了。

未回答的问题

Flux只告诉我们了如何去管理数据流,它并没有回答下面这些问题:

  • 如何从服务器上读取和保存数据?
  • 我应该如何处理组件和非组件之间的通信?
  • 我应该用什么样的事件库呢?
  • 为什么Facebook还没有通过库的方式发布Flux?
  • 我是否应该在store中启用一个数据层(像Backbone)?

这些问题的统一的答案是:随你便!

就是这些了

访问Facebook提供的Example Flux Application作为附加学习资源。希望在读完本文后,js/目录下的文件会变得更容易理解吧。

Flux文档里面包含了一些非常有用的知识(原文:金块nuggets),这些知识点被埋在了撰写不易文档深处。

这篇文章帮助你理解Flux,加我的推吧。