翻译:莫铭
原文地址:Flux-In Depth Overview
Flux是Facebook用来构建客户端web应用的应用框架。它使用单向数据流来补充React的可组合视图组件。它更像是一种模式,而不是一个正式的框架,你可以立即开始使用Flux,而不需要大量的新代码。
Flux应用有三个主要部分: dispatcher, stores, 和views(React 组件)。不要将他们和Model-View-Controller混为一谈。Controllers在Flux应用中是不存在的,他们以controller-views的形式存在(就是一种view,常见于层次结构的顶层,他们从stores接收数据,然后将数据向下传给他们的子孙窗体)。另外,action的生成器(dispatcher的helper函数)作为语义API用来描述应用中可能出现的变化)。为方便理解也可以将它们视为Flux更新循环中的第四部分。
Flux避开MVC,而更倾向于单向数据流。当用于与React view交互时,view会通过dispatcher将action传递给每个store(这些store持有应用的数据和业务逻辑,并且更新那些受影响的views)。 这与React的声明式编程风格特别相似,它允许store发送更新,而无需指定如何在states之间转换views。
我们从恰当地处理派生数据开始:比如,我们想显示消息线程的未读信息数,而另一个视图显示线程列表,高亮显示出那些有未读消息的线程。使用MVC处理这种情况就比较困难(标记一个线程可读,将会更新线程模型,进而需要更新未读数模型)。这些依赖和级联更新在大型的MVC应用中经常发生,致使数据流错综复杂,以及不可预测的结果。
Control和stores是相反的:stores接收更新,并根据需要进行调整,而不是依赖外部以固定的方式来更新它的数据。只有store自己(内部)才知道如何管理它所管理的数据,这有助于保持清晰的职责划分。Stores没有直接的setter函数(比如setAsRead()),取而代之的是在dispatcher中注册回调函数,来获取新的数据。
结构与数据流
Flux应用中的数据单向流动:
单向数据流是Flux模式的核心,上图应该是Flux程序员心中主要的模型。dispatcher,stores和views是具有清晰输入和输出的独立节点。actions是一个简单的objects,包含新数据和一个标识类型的属性。
views会根据用户的交互,生成一个新的action在系统中传播:
所有数据流经作为中央集线器的dispatcher。Actions通常来自于用户与视图的交互,并且在一个action创建器函数中提交给dispatcher。然后dispatcher调用store已经在dispatcher中注册好的回调函数,store会回应那些与它维护的state相关的actions。然后,stores会发出一个更改事件,来告知controller-views,数据层发生了变化。Controller-views监听这些事件,并在一个事件处理器(event handler)中从store获取数据。controller-views调用他们自己的setState()函数,引起自身以及组件树中自己的后代的重新渲染。
这种结构让我们很容易理解我们的应用,以功能反应式编程或者数据流式编程的方式,数据在应用中以固定的方向流动,而不是双向绑定。应用state只在store中维护,从而使应用的不同部分保持高度解耦。依赖只发生在store之间,通过dispatcher管理的同步更新,确保他们被留在严格的层次结构中。
我们发现双向数据绑定会导致级联更新,也就是改变一个对象,将会导致另一个对象的改变,进而触发更多的更新。随着应用规模的增长,这些级联更新将会导致用户交互结果的不可预测。但如果更新只会在一轮内更改数据,整个系统就会变得更加可预测。
让我们仔细看看Flux的各个部分。dispatcher是一个好的切入点。
单独的Dispatcher
在一个Flux应用中,dispatcher是管理数据流的中央枢纽。它并没有真实的智能,本质上只是一个回调到store的注册表。每个store注册自己,并提供一个回调函数。当一个action创建器将一个新的action提供给dispatcher,应用中所有的stores,通过注册的回调函数接收到该action。
随着应用规模的增长,dispatcher变得更为重要,因为它可以通过这些注册的回调函数,不同的调用顺序,来管理stores间的依赖关系。store可以声明式的等待其他store完成更新后,再相应的更新自己。
Facebook在产品用用到的同款dispatcher现在可通过npm, Bower或GitHub获得。
Stores
Stores拥有应用的state和逻辑。他们的角色有点类似于传统MVC中的模型model,但是他们管理很多对象的状态,不过不像ORM模型,他们不代表一条单独的记录数据。它们与Backbone的集合也不相同。除了简单的管理ORM风格的对象集合外,store管理应用中特定域的应用状态。
比如,Facebook的视频回放编辑器利用一个TimeStore管理回访时间点和回放状态。另一方面,应用的ImageStore管理图片集合。在我们TodoMVC示例中的TodoStore和他们差不多,它管理一个待办项集合。一个store展现出的特性就是模型的集合和一个逻辑模块单件。
如上所述,一个store通过dispatcher注册自己,并提供一个回调函数。这个回调函数接收action作为参数。在store注册的这个回调中,通过switch语法,判断action的类型,用来区分action,来调用store的内部方法。在store更新后,他们广播一个事件,来生命他们的状态发生了改变,致使views可以查询到新的状态,然后更新自己。
Views和Controller-Views
React提供了视图层所需的可组合且可自由重新绘制的视图。在嵌套的视图层次结构顶部,一种特殊的视图监听器,监听那些依赖的store所广播的事件。我们称其为controller-view,因为它提供了粘合代码,用来从stores获取数据,然后将这些数据向下传给它的后代链。我们可能会有一个用来管理页面中任何重要部分的controller-view。
当它从store接收事件,它首先通过store公开的getter函数请求它需要的新数据。然后调用自身的setState()或forceUpdate()函数,致使运行自身的render()函数和它所有子孙的render函数。
我们一般将store的整个状态放在一个单独的对象中沿着view链向下传递,允许不同的子孙使用他们需要的数据。除了将类似controller的行为保留在层次结构的顶部,我们还要尽可能的使我们的后代视图尽可能的只是功能,将store的整个状态放在一个单独的对象中向下传递,也有助于减少我们需要管理的props的数量。
偶尔我们可能也需要在更深的层次结构中添加额外的controller-views来保持组件的简单。这会帮助我们更好的封装层次结构中与特定数据域相关的部分。然而请注意,深层次的controller-views可能会由于引入了新的数据流入口,从而违背单一数据流的原则。在决定是否添加深度控制器视图时,需要在获取更简单的组件和不同点流入层次结构的多数据更新的复杂性进行之间进行平衡。这些多数据更新将导致奇怪的效果,由于React的渲染函数被不同的controller-views重复的更新调用,从而可能增加调试的复杂度。
Actions
dispatcher公开一个方法,让我们触发一个派送到stores,并且包含一个装载的数据,我们称其为action。action的创建可以被包装在一个语义helper函数中,将action发送到dispatcher。比如,在一个待办列表应用中,我们打算改变一个待办项的文本。我们可以在我们的TodoActions模块中创建一个action以及一个函数签名为updateText(todoId, newText)的函数。整个函数可以在我们的视图事件处理器中调用,这样我们就可以在用户交互的响应中调用到它了。这个action创建函数也可以添加一个类型到action中,这样当store解析它时,可以得到适当的处理。在我们的李子中,这个类型可以命名为TODO_UPDATE_TEXT.
Actions也可能来自其他地方,比如服务器。比如,发生在数据初始化时。也可能放生在服务器返回一个错误码或服务器提供更新给应用。
Dispatcher哪?
正如之前所提到的,dispatcher也可以管理stores间的依赖。可以用Dispatcher类的waitFor()函数来实现此功能。在TodoMVC application这么简单的应用中,我们确实不需要使用这个方法,但在更为大型和复杂的应用中,他非常的重要。
在TodoStore注册的回调函数中,我们可以明确的等待依赖项先去更新,然后自己再往下执行:
case 'TODO_CREATE':
Dispatcher.waitFor([
PrependedTextStore.dispatchToken,
YetAnotherStore.dispatchToken
]);
TodoStore.create(PrependedTextStore.getText() + ' ' + action.text);
break;
waitFor()接收一个单独的参数,该参数是dispatcher注册索引数组,通常被称作dispatch令牌。因此,调用waitFor()的store可以依赖另一个store的状态来了解如何更新自己的状态。
一个dispatch令牌是向Dispatcher注册回调函数时由register()返回的:
PrependedTextStore.dispatchToken = Dispatcher.register(function (payload) {
// ...
});
更多关于waitFor(),actions,action创建器和dispatcher,请查阅Flux:Actions和Dispatcher.