flux overview
概览
Flux是一个应用框架,Facebook用它来构建客户端的应用。它利用一个单向数据流组织React的组件化视图的组件。相比框架来说它更像是一个模式,你可以不用很多新代码就可以立即开始使用Flux。
Flux应用有三个主要部分:调度器(dispatcher),存储(store)和视图(view,React的组件)。这不应该与MVC混为一谈。在Flux应用中没有控制器,但有控制器视图——视图常常处于层级顶端从store中接收数据,然后再传递给它的子项。另外,动作构造器——调度器的帮组方法——用于提供一个语义接口来描述应用中所有可能的变更。最好把它看作Flux更新环节中的第四部分。
Flux没有使用MVC而是使用了单向数据流。当一个用户与React视图交互的时候,视图通过中心调度器传递一个动作给各种保有应用数据和业务逻辑的store(仓库),然后来更新受影响的视图。这种做法很好尤其是用React的说明性编程样式,允许store发送变更不用指定如何在状态间切换视图。
我们原本打算使用衍生数据来适当的处理:例如,我们想要为邮件显示一个未读计数器同时另一个视图展示邮件列表,未读的邮件被高亮显示。这种情况在MVC里很处理——标记一个单独的邮件为已读我们需要更新邮件模型,然后还要更新未读计数器模型。在一个大的MVC应用中经常有依赖与级联更新的情况发生,导致数据流紊乱与不可预料的结果。
控制器与存储是相对应的:存储接收变更然后合理的调解他们,比依赖于外部的一些东西来更新它们的数据这种方式更具一致性。存储外部没有什么地方干涉它内部的数据变更,很好的实现了关注分离。存储没有像setAsRead()这样直接的设置器方法,相反它只有一种方式取新数据在它的子包含世界中——调度器注册的回调中。
结构与数据流
数据在Flux应用中只有一个单一流向:
单向数据流是Flux模式的核心,上图应该是Flux程序要的心智模式。调度器,存储与视图是独立的节点with distinct输入和输出。动作是带有新数据与表示类型属性的简单对象。
视图可能会引起一个新动作在系统中传递来响应用户的交互:
所有的数据流都要通过调度器作为中心总线。动作在一个动作构造器中提供给调度器,经常从用户交互触发在视图中。调度器然后调用存储注册的回调函数,分发动作到所有的存储中。在它们注册的回调中,存储应答与他们维护状态相关的动作。存储然后触发一个变更事件来告诉控制器视图一个数据层的变更发生了。控制器视图监听他们的事件并在时间处理器中接收stroe的数据。控制器视图调用它们自己的setState()方法,引起它们自身及其关联组件树的重渲染。
这个结构让我们很容易的想起关于我们的应用是功能反应编程,或更特殊的数据流编程或基于流的编程,数据在应用用单向的流动——没有做双向数据绑定。应用的状态只在存储中维护,让应用的各部分高度解耦。发生在存储之间的依赖,它们保持着严格的层级关系,由调度器来管理同步更新。
我们发现双向绑定会引起级联更新,也就是改变了一个对象导致其他对象跟着改变,也会引起更多的变化。随着应用的增长,级联更新会导致由于用户的交互产生难以预料的结果。当数据只能在一个单一的圈内变化时,整个系统就变得可预料了。
让我来深入的了解下Flux的各个部分。当然是从调度器开始比较好些。
一个单一调度器
调度器作为中心总线来管理Flux应用的所有数据流。实际上只是注册回调到存储,它们自己没有真正的逻辑处理——这是一个简单的机制来分配动作到存储中去。每个存储注册自己并提供一个回调。当一个动作构造器提供一个新动作给调度器,所有的存储都会通过回调来接收动作。
随着应用的增长,调度器变得越来越重要,就好比它可以通过安排注册的回调的调用顺序来管理存储之间的依赖关系。存储可以声明等待其他存储更新完毕后在更新自己。
Facebook在生产环境使用的调度器可以通过npm,Bower或GitHub获取。
存储
存储包含应用的状态与逻辑。它们的角色有点像MVC中的M(模型),但他们管理更多对象的状态——它们不像ORM模型那样表示一个单一的数据记录。或者说它们像Backbone的集合(collection)。相比简单的管理ORM样式的对象集合,存储为应用的个别域管理应用的状态。
例如,Facebook的Lookback Video Editor利用TimeStore记录用户的回放时间位置与回放状态。从另一方面来说,同一应用的ImageStore记录图片集合。在我们的TodoMVC实例如,TodoStore管理to-do项目的集合。一个存储在逻辑域中既展示模型集合的特征有展示单个模型的特征。
就像前面讲的,一个存储使用调度器注册并通过回调提供自己。回调接收动作作为一个参数。在存储注册的回调中,一个基于动作类型的开关用于中断动作,且为存储内容方法提供合理的钩子。允许动作通过调度器导致存储状态的更新。 存储更新后,它们广播事件来说明它们的状态已经更新,然后视图会去查询新的状态并更新自身。
视图与控制器视图
React为视图层提供了这种组件化且可自由重绘的视图。接近于嵌套视图层级的顶端,一种特殊的视图监听存储及其依赖所广播的事件。我们称其为控制器视图,它提供胶水代码,用来从存储中获取数据并沿着依赖链传递下去。我们希望使用这些控制器视图中的一个来统筹管理页面的各个部分。
当它从存储获取到事件,它首先通过存储的公共获取方法请求新的数据。接着调用它自己的setState()或forceUpdate()方法,触发其及其依赖的render()方法。
我们经常把存储的整个状态作为一个单独的对象在视图层级链中传递,让不同的后代视图使用它们自己需要的数据。另外在层级顶端保持类控制器的行为,且尽量保持后代视图功能单一,把装个状态作为一个单一对象向下传递还有助于减少我们要管理属性的数量。
偶尔我们需要在更深的层级添加额外的控制器视图来保持组件的简单。这也许帮助我们更好的封装与特殊数据域关联的层级片段。然而在层级中深层的控制器视图会违数据的单一流向原则,会为数据流引入一个新的,可能冲突的入口。要判断是否需要添加深层的视图控制器,平衡一下简单组件与多数据更新的复杂性在层级间流动在不同的点。(前面这两句实在是理不清楚了。)多数据更新会导致奇怪的影响,React的render方法被更新的视图控制器反复的调用,会增加调试的难度。
动作
调度器会暴露一个方法用来触发一个调度到存储中,且包括数据的有效载荷,我们称其为动作。动作的创建会被包裹在一个语义帮助方法中,它发送动作到调度器。例如,我们想要改变任务应用中任务项的文字。我们可以在我们的TodoActions模块中创建一个如updateText(todoId, newText)这样带有一个方法签名的动作。这个方法会在视图事件处理器中被调用,所以我们可以在用户交互的响应中调用它。这个工作构造器还为这个动作添加了一个类型,所以当动作在存储中被解释的时候,它可以合理的响应。在我们的实例中,类型也许被命名成像TODO_UPDATE_TEXT一样。
动作也就可能从其他地方触发,如服务器。总会发生的,如果在数据初始化期间。在当服务返回错误码的时候或服务给应用提供的数据变化时,也可能会发生。
调度器是什么鬼?
如我们先前说的,调度器还可以管理不同存储之间的依赖。这个功能可以通过在Dispatcher类中的waitFor方法来实现。我们在简单实例TodoMVC应用中不需要这个方法,但在一个大的,复杂的应用它就比较重要了。
1 case 'TODO_CREATE': 2 Dispatcher.waitFor([ 3 PrependedTextStore.dispatchToken, 4 YetAnotherStore.dispatchToken 5 ]); 6 7 TodoStore.create(PrependedTextStore.getText() + ' ' + action.text); 8 break;
waitFor()接受一个单独的参数,即一个调度器注册索引的数组,通常叫做适配标识。于是一个存储调用waitFor()可以依赖于另一个存储来通知它如何更新自己的状态。
一个适配标识就是register()的返回值,当为调度器注册一个回调的时候。
1 PrependedTextStore.dispatchToken = Dispatcher.register(function (payload) { 2 // ... 3 });
关于更多关于waitFor(), 动作,动作构造器与调度器的信息,请阅读Flux:Actions and the Dispatcher。