原文地址:https://blog.andrewray.me/flu…,作者:Andrew Ray
TL;DR 当我在努力学习 Flux 时,我希望有人告诉我:它并不简单,也没有好的文档可以查,并且有许多变化的部分。
我需要使用 Flux 吗?
如果你的应用程序需要处理动态数据(dynamic data)的话,那么答案就是 yes,你可能需要使用 Flux。
如果你的应用程序仅仅是无需共享状态静态视图(static view),并且你从不保存也不更新数据,那么你不需要使用 Flux,Flux 不会给你带来任何好处。
为什么是 Flux?
皮一下,因为 Flux 是个适度复杂的主意,为啥要增加复杂度呢?
90%的 iOS 应用程序是表格视图中的数据。iOS 工具包具有良好定义的视图和数据模型可以让应用开发变得简单。
但是在前端(Font End:HTML,JS,CSS),我们甚至都没有。相反,我们遇到一个很大的问题:没有人知道应该如何去构建一个前端应用。我从事这个行业多年,从来没人教给我“最佳实践”,相反,他们教了我好多“库(libraries)”,诸如 jQuery,Angular,Backbone 等等。但是真正的问题、数据流,仍然避开了我们。
什么是 Flux?
Flux 是一个用来描述具有非常特定事件和监听的“单向”数据流的词。没有官方的 Flux 库,但是你需要 Flux Dispatcher 和任何的 JavaScript event library。
官方文档写的就像某人的意识流一样,从这里开始学习是不太好的。但是一旦你掌握了 Flux,它可以帮助你填补空白。
不要试图把 Flux 同 MVC 架构进行比较,它们的相似之处只会令人困惑。
正式入坑!我将按顺序解释概念,并且一个一个地构建它们。
1. 视图的“Dispatch”和“Actions”
Dispatcher(调度员)本质上是一个加入了额外规则的事件系统。它来广播事件并注册回调。全局的 dispatcher 只有唯一的一个,你应该使用 Facebook Dispatcher Library。实例化非常容易:
var AppDispatcher = new Dispatcher();
假设你的应用程序有一个“新建”按钮来向列表添加项目。
<button onClick={this.createNewItem}>New Item</button>
点击会发生什么?你的视图会调度一个非常具体的“操作”,其中包含操作名称和新项目数据:
createNewItem: function(evt) {
AppDispatcher.dispatch({
actionName: ‘new-item’,
newItem: {name: ‘Marco’} // example data
});
}
“action”是 Facebook 创造的另一个词。它是一个 JavaScript 对象,用以描述我们想要做什么事情,以及做这件事我们需要的数据。正如你所见到的,我们要做的事情就是添加一个 new-item,我们需要的数据就是项目 name。
2.”Store” 响应调度的操作
像 Flux 一样,“Store”这个词也是 Facebook 创造的. 对于我们的应用程序,我们需要列表的特定逻辑和数据集合。这描述了我们的 Store,我们称之为 ListStore。
Store 是一个单体对象,意味着你可能不能通过“new”关键字来声明它,应用程序中每个 Store 里只有一个实例。
// Single object representing list data and logic
var ListStore = {
// Actual collection of model data
items: []
};
然后,Store 会响应已分派的操作:
var ListStore = …
// Tell the dispatcher we want to listen for *any*
// dispatched events
AppDispatcher.register(function( payload) {
switch(payload.actionName) {
// Do we know how to handle this action?
case ‘new-item’:
// We get to mutate data!
ListStore.items.push(payload.newItem);
break;
}
});
这是 Flux 处理调度回调的传统方式。每个 payload 包含一个 action 的名称(actionName)和数据(newItem),switch 语句确定 Store 是否应该响应 action,并且知道根据 action 的类型处理数据变化。
???? 关键点:store 不是数据模型,一个 Store 包含模型。???? 关键点:store 在你的应用程序中唯一知道如何更新数据的东西,它是 Flux 中最重要的部分。我们调度的 action 并不知道如何添加或者删除项目。
举个栗子,假如应用程序中不同的部分需要保持跟踪某些图片及其元数据,那么你就需要创建其他的 store,并将其命名为 ImageStore。一个 store 相当于应用程序中一个单独的“域(domain)”,如果应用程序非常庞大,这些域可能对你来说已经很明显了。如果应用程序很小,你可能只需要一个 store。一般来说,一种模型类型只对应一个 Store。
只有 store 允许注册 Dispatcher 的回调!view 永远不应该调用 AppDispatcher.register。Dispatcher 应该只用于将消息从视图 View 发送到 Store。视图(view)会响应不同类型的事件。
3. Store 触发“Change”事件
即将完成!现在数据确实已经变化了,我们需要告诉全世界!Store 触发一个事件(Event),但是不会使用 dispatcher。这虽然令人困惑,但是这就是 Flux 的方式。让我们给我们的 Store 加入触发事件的能力。如果你正在使用 MicroEvent.js,那么很简单:
MicroEvent.mixin(ListStore);
然后,触发 changes 事件
case ‘new-item’:
ListStore.items.push(payload.newItem);
// Tell the world we changed!
ListStore.trigger(‘change’);
break;
???? 关键点:当我们触发事件的时候,我们不会传递最新的项目。视图 View 只关心有事情发生变化了。让我们继续关注数据以了解原因。
4. 视图(View)响应“Change”事件
现在我们需要展示列表。当列表发生变化时,视图会完全地重新渲染(re-render)。
首先,当组件“安装(mount)”时,即组件首次被创建的时候,从 ListStore 中监听 change 事件:
componentDidMount: function() {
ListStore.bind(‘change’, this.listChanged);
},
为简单起见,我们只调用 forceUpdate,它可以触发重新渲染(re-render)
listChanged: function() {
// Since the list changed, trigger a new render.
this.forceUpdate();
},
当组件“卸载(unmount)”的时候,不要忘记清除事件监听器
componentWillUnmount: function() {
ListStore.unbind(‘change’, this.listChanged);
},
现在怎么办?让我们来看看我的 render 函数,我故意将其保存到最后。
render: function() {
// Remember, ListStore is global!
// There’s no need to pass it around
var items = ListStore.getAll();
// Build list items markup by looping
// over the entire list
var itemHtml = items.map(function( listItem) {
// “key” is important, should be a unique
// identifier for each list item
return <li key={listItem.id}>
{listItem.name}
</li>;
});
return <div>
<ul>
{itemHtml}
</ul>
<button onClick={this.createNewItem}>New Item</button>
</div>;
}
现在已经完整了。当你添加新项目的时,View 发出用户的 Action,Dispatcher 收到 Action,要求 Store 进行相应的更新,store 改变数据,然后 store 会触发 change 事件,最后视图通过重新渲染页面来响应 change 事件。
译者注:原文无此图
但这里有一个问题:每次列表更改时我们都会重新渲染整个视图!这不是非常低效吗?
不。
当然,我们将再次调用 render 函数,并确保渲染函数中的所有代码都将重新运行。但是,如果渲染输出已更改,React 将仅更新真实 DOM。您的 render 函数实际上是生成一个“虚拟 DOM”,React 与之前的输出进行比较 render。如果两个虚拟 DOM 不同,React 将仅使用差异更新真实 DOM.
???? 关键点:当 Store 的数据改变时,视图不应该关心是否有东西被添加,删除,或是被改变了。视图应该只去做重新渲染。React 的“虚拟 DOM”差异算法会去处理这些重大问题,找出那些真正发生变化的 DOM 节点。这会让您的生活更加简单,并降低您的血压。
还有一件事:“Action Creator”到底是个啥?
记住,当我们点击按钮的时候,会分配一个具体的动作(action):
AppDispatcher.dispatch({
eventName: ‘new-item’,
newItem: {name: ‘Samantha’}
});
好吧,如果您的许多视图需要发送此操作,则可以重复输入。此外,您的所有视图都需要知道特定的对象格式。那太蹩脚了。Flux 建议一种抽象,称为 Action Creator,它只是将上述内容抽象为一个函数。
ListActions = {
add: function(item) {
AppDispatcher.dispatch({
eventName: ‘new-item’,
newItem: item
});
}
};
现在您的视图可以调用 ListActions.add({name: ‘…’});,而不必担心调度的对象语法。
PS:不要使用 forceUpdate
我因为习惯了 forceUpdate 这个简单的缘故。组件读取 store 数据的正确方法,是将数据拷贝到 state, 并且在 render 函数中读取 this.state。您可以在 TodoMVC example 中看到它的工作原理。
首次加载组件时,store 的数据被拷贝到 state 中, 当 store 更新时候, 数据被完整地重新拷贝。这样做是更好的,因为在内部,forceUpdate 是同步的,同时 setState 的效率也是非常高的。