看 flux 官网的文档还是有些云里雾里的,需要一个 demo 来帮助在实践中进行理解,果断要选择 todomvc。
todomvc 已经有了各种各样框架的实现版本,在 flux 官方仓库中也提供了 tomomvc 的 flux� 版本,拿它作为入门学习 flux 的 demo 是相当不错的。
本文仅仅使用 todomvc 作为一个例子来说明,不会对其进行具体的代码解析。
官方提供的 demo 中 js 脚本的目录大致如下:
|- actions/
|- components/
|- constants/
|- dispatcher/
|- stores/
|- app.js
恰如 flux 文档提到的,dispatcher, store,view (即是 react component)是 flux 中三个主要的部分。 constants 主要是提供一些常量,actions 是抽象化的用户操作,即定义为一个对象,包括了操作的类型以及数据。 每一部分的内容存放在各自的文件夹中,app.js 是应用入口文件,主要内容比较简单:
React.render(
<TodoApp />,
document.getElementById('todoapp')
);
入口文件主要就是渲染了 TodoApp
这个 component,TodoApp
可以理解为官网文档提到的 control-view,用于监听数据层,即 stores 的变化,然后将数据传递给它的子元素。
它会调用对应的 stores 来获取初始化时需要的数据,拿到现在保存的所有 todo 项,以及他们是否都完成了:
function getTodoState() {
return {
allTodos: TodoStore.getAll(),
areAllComplete: TodoStore.areAllComplete()
};
}
...
getInitialState: function() {
return getTodoState();
},
获取到的数据作为 control-view 的 state 对象,并且将其作为子组件的 props 传递过去:
...
<MainSection
allTodos={this.state.allTodos}
areAllComplete={this.state.areAllComplete}
/>
<Footer allTodos={this.state.allTodos} />
子组件便可以获取需要的数据,同时,control-view 会监听对应 store 的变化,数据变化时,获取更新后的数据,重新设置自己的 state。 随着 control-view 的 state 的更新,会把数据传递给子组件,子组件也会获取到新的数据来进行更新。
componentDidMount: function() {
TodoStore.addChangeListener(this._onChange);
},
...
_onChange: function() {
this.setState(getTodoState());
}
注意,虽然 TodoApp
给人感觉很像是 controller,但它还是 view 的范畴,stores 的变化引发 view 的变化,数据还是如同 flux 追求的一样,单向流动,只是其间是多了子组件的更新,如同 store -> view -> subviews
,不过子组件的更新都由自身完成,在封装好的时候你不用再对其做什么处理。
这里延续上一部分的内容,我们来看一下 MainSection
,它主要做的就是把一条一条的 todo 显示出来,并且提供一个完成全部的按钮。
其中,每一条 todo 便是一个 TodoItem
的 component,这个先不详述,我们关注点在 action,即完成全部的按钮上:
<input
id="toggle-all"
type="checkbox"
onChange={this._onToggleCompleteAll}
checked={this.props.areAllComplete ? 'checked' : ''}
/>
...
_onToggleCompleteAll: function() {
TodoActions.toggleCompleteAll();
}
按钮 change 时调用 TodoActions
的一个方法,所以说 action 是用户操作的一个抽象。
TodoList 中每一个 TodoItem
都有完成和删除按钮,它们也有对应的 action 方法。用户操作时,调用对应的 action 方法,这里便完成了一个 view -> action
的流程。
回到完成全部按钮上,当用户点击时,假设按钮状态为 checked,那么你知道数据中的所有 todo 项都要是完成状态,我们需要知道,action 方法有什么作用。
简单查看一下上述调用到的 toggleCompleteAll()
方法:
toggleCompleteAll: function() {
AppDispatcher.dispatch({
actionType: TodoConstants.TODO_TOGGLE_COMPLETE_ALL
});
},
action 做的事情很简单,就是把抽象的操作类型的数据给到 dispatcher 就好了。这里的操作不需要其他数据,只有一个操作类型。
在 flux 中,dispatcher 是一个单例,todomvc 中的 TodoStore
会在 AppDispatcher
注册一个回调:
AppDispatcher.register(function(action) {
var text;
switch(action.actionType) {
case TodoConstants.TODO_CREATE:
text = action.text.trim();
if (text !== '') {
create(text);
TodoStore.emitChange();
}
break;
...
当 action 发生时,调用 dispatch
方法来传递抽象的 action 数据,新的数据会来到 dispatcher 这里,这个时候执行 store 注册的回调方法,来操作 store。
这里会让人有疑惑,为什么需要 dispatcher,并且是 store 注册回调的方式?为何不使用 action 直接操作 store 呢?
个人感觉 dispatcher 的设计是精髓。
假设我们对 todomvc 进行功能扩展,需要引入新的数据,但是操作不用添加,如果是使用 action 来操作 store,那么我们需要在原有的 action 方法里引入新的 store,然后调用对应的方法。 但是在真正的 flux 中则不需要,我们在创建新的 store 时,往 dispatcher 中注册一个新的回调便可以了。
dispatcher 的这种方式,让 store 来决定对哪些 action 需要进行响应,而不是由 action 来决定更新哪些数据,一个 action 可能涉及大量的数据变动,但是都由各个部分的 store 分别管理起来。
当应用变得越来越大时,会出现 store 之间的依赖,类似上边提到的,我们可能会添加新的 store,dispatcher 可以用于帮助管理 store 之间的依赖,它提供了 waitFor
方法,传入注册的回调 token 便可以确保对应的回调已经执行。
我们回到 todomvc 中,上一部分的 action 会触发回调,执行的代码如下:
case TodoConstants.TODO_TOGGLE_COMPLETE_ALL:
if (TodoStore.areAllComplete()) {
updateAll({complete: false});
} else {
updateAll({complete: true});
}
TodoStore.emitChange();
break;
updateAll()
是 store 里边的方法,我们待会再提,这里便是调用 store 进行数据更新,然后 emitChange()
触发 store 的 change 事件。
stores 在 flux 里边定义便是数据管理那一层,便是处理数据的增删改查,如 TodoApp
在初始化时从 TodoStore
中获取了初始化需要的数据。
而上边提到的 updateAll()
方法,则是更新所有的 todo 项。看一下代码:
function update(id, updates) {
_todos[id] = assign({}, _todos[id], updates);
}
...
function updateAll(updates) {
for (var id in _todos) {
update(id, updates);
}
}
这里便是更新 store 内部的 _todos 数据了。这里仅仅做数据相关的东西,记得上一部分的内容,store 会往 dispatcher 注册一个回调来相应相关的 action。
而这个回调方法,会调用 store 的内部方法更新数据,同时触发 store 的 change 事件,而你应该记得,view 中是会响应 store 的 change 事件的。
这是一个圈。
我们来整理一下,整一个流程是怎么样的:
初始化时,便是 render 一个总的 app component,而渲染时候需要的数据都是从 store 中获取,我们可以看成是从 store 出发:
- store:准备好初始化需要的数据,注册 dispatcher 回调函数 ->
- view:将数据渲染到页面中,注册 store change 响应函数 ->
- action:用户操作,调用 action 对象的方法,传入操作相关数据 ->
- dispatcher: action 中的方法调用 dispatcher.dispatch 分发 action,执行回调函数 ->
- store:dispatcher 中的回调方法调用 store 的方法更新数据,触发 store change 事件 ->
- view:响应 store change 事件,根据 store 中的数据更新组件状态并且更新页面展示 -> 3 ...
我们可以清晰地看到,单向数据流动,数据从 store 初始化,流向 view 进行渲染,然后用户的操作导致数据变更了,数据从 action 开始,流向 dispatcher,dispatcher 再将数据交由 store 进行更新,然后数据流向 view。
虽然其中省去了一些东西,但是大致的 flux 结构流程我是描绘出来了,最后再看一下这个图:
👍