与 React 相同,Flux 同样由一群 Facebook 工程师提出,它的名字是拉丁语的 Flow。Flux 的 提出主要是针对现有前端 MVC 框架的局限总结出来的一套基于 dispatcher 的前端应用架构模 式。如果用 MVC 的命名习惯,它应该叫 ADSV(Action Dispatcher Store View)。
那么 Flux 是如何解决 MVC 存在的问题呢?正如其名,Flux 的核心思想就是数据和逻辑永 远单向流动。其模型图如图。
在介绍 React 的时候,我们也提到它推崇的核心也是单向数据流,Flux 中单向数据流则是在整体架构上的延伸。在 Flux 应用中,数据从 action 到 dispatcher,再到 store,最终到 view 的路线是单向不可逆的,各个角色之间不会像前端 MVC 模式中那样存在交错的连线。
然而想要做到单向数据流,并不是一件容易的事情。好在 Flux 的 dispatcher 定义了严格的规则来限定我们对数据的修改操作。同时,store 中不能暴露 setter 的设定也强化了数据修改的纯洁性,保证了 store 的数据确定应用唯一的状态。
再使用 React 作为 Flux 的 view,虽然每次 view 的渲染都是重渲染,但并不会影响页面的性能,因为重渲染的是 Virtual DOM,并由 PureRender 保障从重渲染到局部渲染的转换。意味着完全不用关心渲染上的性能问题,增、删、改的渲染都和初始化渲染一样快。
对于一些逻辑复杂的前端应用(比如 Firefox 中的调试器),Flux 已经证明了自己确实能够极
大地降低复杂度。但是对于许多原本使用 MVC 方式架构都绰绰有余的项目来说,Flux 看起来像
是杀鸡用牛刀。
Flux 基本概念
了解了为什么我们会选择 Flux 模式之后,下面来讲述它的基本概念和组成。
一个 Flux 应用由 3 大部分组成——dispatcher、store 和 view,其中 dispatcher 负责分发事件;
store 负责保存数据,同时响应事件并更新数据;view 负责订阅 store 中的数据,并使用这些数据
渲染相应的页面。
尽管它看起来和 MVC 架构有些像,但其中并没有一个职责明确的 controller。事实上,Flux
中存在一个 controller-view 的角色,但它的职责是将 view 与 store 进行绑定,并没有传统 MVC 中
controller 需要承担的复杂逻辑。
图 4-5 是 Flux 应用的简化执行流程,下面我们将依次介绍各个节点的作用。
- dispatcher 与 action
如果你熟悉 Backbone 的话,肯定对 Backbone 的事件机制印象深刻。与 Backbone 的发布/订
阅模式不同,Flux 中的事件会由若干个*处理器来进行分发,这就是 dispatcher。
dispatcher 是 Flux 中最核心的概念,也是 flux 这个 npm 包中的核心方法。
事实上,dispatcher 的实现非常简单,我们只需要关心 .register(callback) 和 .dispatch
(action) 这两个 API 即可。
register 方法用来注册一个监听器,而 dispatch 方法用来分发一个 action。
action 是一个普通的 JavaScript 对象,一般包含 type、payload 等字段,用于描述一个事件以
及需要改变的相关数据。比如点击了页面上的某个按钮,可能会触发如下 action:
{
“type”: “CLICK_BUTTON”
}
这是 action 最简单的一种形式。在实际应用中,一个 action 还可能包含更多的信息,比如某
个操作对应的用户 ID、当前操作是否出现错误的标志位等。
在开源社区中,有一套关于 Flux 中 action 对象该如何定义的规范,称为 FSA(Flux Standard
Action)①。该规范定义了一个 Flux action 必须拥有一个 type 字段,可以拥有 error、payload 或
meta 字段。除此之外,不能有其他额外的字段。
store
在 Flux 中,store 负责保存数据,并定义修改数据的逻辑,同时调用 dispatcher 的 register 方法将自己注册为一个监听器。这样每当我们使用 dispatcher 的 dispatch 方法分发一个 action 时,在 Flux 中,store 负责保存数据,并定义修改数据的逻辑,同时调用 dispatcher 的 register 方法将自己注册为一个监听器。这样每当我们使用 dispatcher 的 dispatch 方法分发一个 action 时,store 注册的监听器就会被调用,同时得到这个 action 作为参数。store 一般会根据 action 的 type 字段来确定是否响应这个 action。若需要响应,则会根据 action 中的信息修改 store 中的数据,并触发一个更新事件。需要特别说明的是,在 Flux 中,store 对外只暴露 getter(读取器)而不暴露 setter(设置器),这意味着在 store 之外你只能读取 store 中的数据而不能进行任何修改。
controller-view
虽然说 Flux 的 3 大部分是 dispatcher、store 和 view,但是在这三者之间存在着一个简单却不
可或缺的角色——controller-view。顾名思义,它既像 controller,又像 view,那么 controller-view
究竟在 Flux 中发挥什么样的作用呢?
一般来说,controller-view 是整个应用最顶层的 view,这里不会涉及具体的业务逻辑,主要
进行 store 与 React 组件(即 view 层)之间的绑定,定义数据更新及传递的方式。
controller-view 会调用 store 暴露的 getter 获取存储其中的数据并设置为自己的 state,在render 时以 props 的形式传给自己的子组件(this.props.children)。
介绍 store 时我们说过,当 store 响应某个 action 并更新数据后,会触发一个更新事件,这个更新事件就是在 controller-view 中进行监听的。当 store 更新时,controller-view 会重新获取 store 中
的数据,然后调用 setState 方法触发界面重绘。这样所有的子组件就能获得更新后 store 中的数据了。
view
在绝大多数的例子里,view 的角色都由 React 组件来扮演,但是 Flux 并没有限定 view 具体的实现方式。因此,其他的视图实现依然可以发挥 Flux 的强大能力,例如结合 Angular、Vue 等。
在 Flux 中,view 除了显示界面,还有一条特殊的约定:如果界面操作需要修改数据,则必须使用 dispatcher 分发一个 action。事实上,除了这么做,没有其他方法可以在 Flux 中修改数据。
这条限制对刚接触 Flux 的开发者来说难以理解。因为在 React 中需要修改数据的时候,直接调用 this.setState 方法即可。如果需要分发 action,那么 action 是什么样的,分发到哪里,由谁来处理,View 层如何更新?这些疑问我们会在 4.4 节中一一讲解。目前只需要知道 Flux 中的view 层不能直接修改数据就可以了。
actionCreator
与 controller-view 一样,actionCreator 并不是 Flux 的核心概念,但在许多关于 Flux 的例子和
文章中都会看到这个名词,因此有必要解释一下。actionCreator,顾名思义,就是用来创造 action
的。为什么需要 actionCreator 呢?因为在很多时候我们在分发 action 的时候代码是冗余的。
考虑一个点赞的操作,如果用户给某条微博点了赞,可能会分发一个这样的 action:{
type: ‘CLICK_UPVOTE’,
payload: {
weiboId: 123,
},
}
而包含完整分发逻辑的代码更加复杂:
import appDispatcher from ‘…/dispatcher/appDispatcher’;
// 响应点赞的 onClick 方法
…
handleClickUpdateVote(weiboId) {
appDispatcher.dispatch({
type: ‘CLICK_UPVOTE’,
payload: {
weiboId: weiboId,
},
});
}
…
事实上,在分发 action 的 6 行代码中,只有 1 行是变化的,其余 5 行都固定不变,这时我们
可以创建一个 actionCreator 来帮减少冗余的代码,同时方便重用逻辑:
// actions/AppAction.js
import appDispatcher from ‘…/dispatcher/appDispatcher’;
function upvote(weiboId) {
appDispatcher.dispatch({
type: ‘CLICK_UPVOTE’,
payload: {
weiboId: weiboId,
},
});
}
// components/Weibo.js
import { upvote } from ‘…/actions/AppAction’;
…
handleClickUpdateVote(weiboId) {
upvote(weiboId);
}
…
可以看到,在 view 中,分发 action 变得异常简洁。同时当我们需要修改 upvote 的逻辑时,
只需要在 actionCreator 中进行修改即可,所有调用 upvote 的 view 都无需变动。