React Flux的一些理解(React Flux入门教程)

Flux is the application architecture that Facebook uses for building client-side web applications. It complements React's composable view components by utilizing a unidirectional data flow. It's more of a pattern rather than a formal framework

Flux是一种模式, 来描述单向数据流.

Flux主要包括三个部分: dispatcher, stores 和 views(React的组件).

Flux primary mental

上面这张图是使用Flux的首要思考模型. ispatcher, stores 和 views 是有着特定输入输出的独立节点.

views根据用户的交互动作有可能会产生一个新的action: Flux primary mental

Dispatcher就像一个中心枢纽所有数据的流动都要通过这里. Action来源于用户与views的交互行为, Action触发Dispatcher. Dipatcher分发这个事件给对应的Store(通过之前注册的回调函数callback). Store在修改State后触发一个"change"事件通知controller-views数据发生变化了. controller-views监听这些"change"事件并且从stores暴露的函数中获取(新)数据, 然后调用自己的setState()方法, rerender自己和它的子组件.

Flux primary mental

A Single Dispatcher

Dispatcher是Flux应用中管理所有数据流的中心枢纽. 它本质上就是一些Store回调函数的注册器, 它本身没有其他逻辑 - 只是提供了把Action分发给Store的机制. (我的理解是: dispatcher根据action type调用对应的回调函数). 每一个Store都在Dispatcher注册(AppDispatcher.register)并提供回调函数.

随着应用的发展, Dispatcher会变得越来越重要. 例如Dispatcher可以用来管理Stores之间的依赖关系,通过特定的顺序来调用注册了的回调函数就可以办到. Stores可以等到其他Stores完成更新再进行自己的更新操作.

Stores

Store内有这个应用的State和对应的逻辑. 它的角色在一定程度上和传统MVC结构中的model相似.

以下这两段话实在不知道肿么翻译了...

A store exhibits characteristics of both a collection of models and a singleton model of a logical domain.

More than simply managing a collection of ORM-style objects, stores manage the application state for a particular domain within the application.

之间已经提过了, Store在Dispatcher注册(AppDispatcher.register)并提供Dispatcher一个回调函数, 这个回调函数接受Action作为参数. 在Store的已注册的回调函数内, 有一个switch声明, 根据action type来提供进入不同Store内部函数的钩子. 这允许了一个Action可以通过Dispatcher来更新Store中的State. 待State更新后, Store广播一个事件来告诉大家State已经更新啦, 然后Views就可以获取新的State并更新自己.

Views and Controller-Views

React在View(MVC)层提供了可组合的可自由重新渲染的Views. 在嵌套的views结构顶部, 一个特别的view监听着stores广播的事件, 我们管这种view叫controller-view.在controller-view中我们完成这样的操作: 从stores中获取数据并且传递这些数据的到它的子代中. 我们总有一个这样的controller-view控制页面的某一部分.

controller-view接受到store广播的事件, 它首先从store的公共getter方法中获取它需要的新数据,然后调起setState()或者forceUpdate()方法,那么它和它所有子代的render()方法都会运行.

我们常常把整个store的state放在一个对象里面传递到子代中, 让子代选择自己需要的东西. 这样除了可以在层级结构顶层保持控制(controller)行为因此尽可能保证子代views的单一功能外, 还可以减少我们需要管理的属性(props)的数目.

有时候我们可能需要在层级结构的某一层建立另外的一些controller-view使一些组件能简单些. 这样可以帮助我们更好地去封装层级上的与特定的数据有关联的一些模块. 请注意, 在不是顶层建立一个controller-view会破坏单项数据流这个原则,因为有可能会存在数据入口的冲突. 在做这样的决定之前, 我们可以衡量一下得到一个简单一点的组件和多重数据流多个数据更新入口孰轻孰重. 多重数据流会有一些副作用: React的render()方法会因为不同的controller-view的数据更新而多次被处罚, 会增加debug的难度.

Actions

Dispatcher暴露了一个方法(dispacher())允许我们在Action中调用然后把Action装载的数据分发到Stores中. 我们可以Actions都封装在一个 Action's Creator中来统一管理.

除了Views, Action也可能来自于server, 例如在数据初始化的过程中, 还有是api返回了错误代码或者需要更新数据的时候.

What About that Dispatcher?

上面说过, Dispatcher可以用来管理Stores之间的依赖关系. 这是通过Dispatcher类中的waitfor()方法实现的.

case 'TODO_CREATE':
  Dispatcher.waitFor([
    PrependedTextStore.dispatchToken,
    YetAnotherStore.dispatchToken
  ]);

TodoStore.create(PrependedTextStore.getText() + ' ' + action.text); break;

waitfor()接受一个参数: 一个dispatcher注册函数的顺序数组, 这里的dispatcher注册函数通常由dispatch tokens代表.

当Dispatcher注册回调函数的时候, register()返回一个dispatch token:

PrependedTextStore.dispatchToken = Dispatcher.register(function (payload) {
  // ...
});

A Flux Example

说了那么久, 我们来看一个例子: Todo List flux-todomvc

下载源码后, npm install, 然后 npm run build, 最后 npm start 就可以运行了. 应用通过使用Browserify进行持续构建.

加载todo项目

Flux primary mental

应用在启动的时候 ToDoApp react组件(/js/components/TodoItem.react.js)获取ToDoStore中的数据. ToDoStore是完全不知晓ToDoApp的.

// Loading the initial data into the application:
// ...
/*
 * Retrieve the current TODO data from the TodoStore
 /
function getTodoState() {
  return {
    allTodos: TodoStore.getAll(),
    areAllComplete: TodoStore.areAllComplete()
  };
}

var TodoApp = React.createClass({

getInitialState: function() { return getTodoState(); },

// ...

这个这么简单的例子我们不需要考虑到ToDoStore是如何加载初始数据的.

创建一个新的ToDo 项目

Flux primary mental

ToDoApp组件在界面上有一个表单让用户输入创建一个新的ToDo项目. 当用户提交表单的时候, 一个数据流通过Flux系统开始传输了, 如上图.

以下描述的过程都是按照上图的顺序进行的.

1.组件利用回调函数处理用户的输入

/js/components/Header.react.js Header 在TodoApp(启动app的组件)中被引用了.

// Saving a new ToDo calls the '_onSave' callback
// ...
var Header = React.createClass({

/* * @return {object} / render: function() { return ( <header id="header"> <h1>todos</h1> <TodoTextInput id="new-todo" placeholder="What needs to be done?" onSave={this._onSave} /> </header> ); }, // ...

2.回调函数调用Action Creator中的函数

第一步中的_onSave函数来处理用户输入: 把用户输入传递给Action Creato中对应的Action函数.

/js/components/Header.react.js

// The 'onSave' callback calls the 'TodoActions' method to create an action
// ...
  /*
   * Event handler called within TodoTextInput.
   * Defining this here allows TodoTextInput to be used in multiple places
   * in different ways.
   * @param {string} text
   /
  onSave: function(text) {
    if (text.trim()){
      TodoActions.create(text);
    }
  }

3.Action Creator 创建一个 action type 为 TODO_CREATE 的action

/js/actions/TodoActions.js

// The 'create' method creates an action of type 'TODO_CREATE'
// ...
var TodoActions = {

/* * @param {string} text / create: function(text) { AppDispatcher.dispatch({ actionType: TodoConstants.TODO_CREATE, text: text }); }, // ...

4.Action被发送到Dispatcher

通过调用AppDispatcher.dispatch()函数. (步骤3中代码)

5.Dispatcher传递action至store注册了的回调函数.

store向dispather注册回调函数: AppDispatcher.register()

/js/stores/TodoStore.js

AppDispatcher.register(function(action) {
  var text;

switch(action.actionType) { case TodoConstants.TODO_CREATE: text = action.text.trim(); if (text !== '') { create(text); } TodoStore.emitChange(); break; // ... }); // ...

6.ToDoStore注册了一个监听TODO_CREATE然后更新数据的回调函数

/js/stores/TodoStore.js

// The TodoStore has registered a callback for the 'TODO_CREATE' action.
// ...
/
 * Create a TODO item.
 * @param  {string} text The content of the TODO
 /
function create(text) {
  // Hand waving here -- not showing how this interacts with XHR or persistent
  // server-side storage.
  // Using the current timestamp + random number in place of a real id.
  var id = (+new Date() + Math.floor(Math.random()  999999)).toString(36);
  _todos[id] = {
    id: id,
    complete: false,
    text: text
  };
}

// Register to handle all updates AppDispatcher.register(function(action) { var text;

switch(action.actionType) { case TodoConstants.TODO_CREATE: text = action.text.trim(); if (text !== '') { create(text); } TodoStore.emitChange(); break; // ... }); // ...

7.更新数据后, TodoStore触发一个"改变事件"

TodoStore.emitChange();

8.TodoApp组件监听TodoStore发出的"改变事件", 然后根据TodoStore最近的数据重新渲染组件.

'/js/components/TodoApp.react.js'

// The component listens for changes and calls the '_onChange' callback
// ...
var TodoApp = React.createClass({

getInitialState: function() { return getTodoState(); },

componentDidMount: function() { TodoStore.addChangeListener(this._onChange); },

componentWillUnmount: function() { TodoStore.removeChangeListener(this.onChange); }, // ... /* * Event handler for 'change' events coming from the TodoStore / onChange: function() { this.setState(getTodoState()); } // ...

Flux vs. MVC

FLUX据说可以代替MVC,官方解释说:

eschews MVC in favor of a unidirectional data flow

Flux因为单向数据流(a unidirectional data flow)避免了使用MVC结构.

在对比两者之前, 我们首先需要清楚几点:

  1. “MVC”实际上代表的是“MV*”
  2. Flux并不比MV*简单
  3. Flux相对于MV*事情变得更加可预见

"MVC"实际上代表的是"MV*"

"MVC" actually means “MV*” in the land of JavaScript.

ToDoMVC 的15个JS框架例子, 没有一个是严格运用了"Model, View, Controller"的设计模式的. Controller这个角色在很多JS框架中都被View或者Model融合掉了, Controller更多地是扮演路由(router)的角色.

MVC大家都很清楚了, 这里不就赘述了, 总的来说就是下面这个经典的图:

Basic MVC Data Flow

Flux并不比MV*简单

Flux is not simpler than MV*.

Facebook intro to Flux 在这个视频中分析了为什么MVC很难具有规模(MVC doesn’t scale), 并给出了这样一张图, 七个不同的models和views之间的数据流:

Complex MVC Data Flow

看到这图的箭头就头晕了. 有谁还可以知道程序里面到底发生了什么. 相对起来, Flux的确是要比较简单.

但是在视频中没有提到在同等规模下Flux的复杂程度. 当然, 因为数据都归纳到一条很简单的数据流中:

Flux primary mental

但是, 在大型的系统中应用Flux的情况也是很值得一看的:

Complex Flux Data Flow

你看, 这图比MV*那图还有更多的箭头和盒子. 所以Flux的程序和MV*的程序看起来同样复杂. 但是不要忘了hen很重要的一点, Flux图中的箭头都是单向的, 在系统内形成了一个连续一致的循环.

Flux让事情变得可预见

Flux keeps things predictable.

在Flux程序和MV*程序同样在内部都有很多事情在时刻发生, 但是Flux程序的可预见性却高得多.

Dispatcher同样保证了每一次只通过一个Action. 如果Dispatcher在完成处理一个action之前又有一个action被传递过来,那么就会报错:

“Uncaught Error: Invariant Violation: Dispatch.dispatch(…): Cannot dispatch in the middle of a dispatch.”

这也是保持可预见性的其中一种办法. 这迫使了开发者在开发过程中不允许有复杂的数据之间的交互.

Dispatcher也提供了指定Stores执行他们的回调函数顺序的方法: waitFor(), 告诉一个Store在执行回调函数之前去等待另一个Store的完成. 当然, 如果你写了一段代码让两个Stores相互等待, 那么Dispatcher就会报错.

在Flux中, 你可以清楚地知道到底是什么导致了数据的变化. 每一个Store都包括了一个它监听的Actions的列表.

// The case statement documents which actions this store listens to
// ...
ThreadStore.dispatchToken = ChatAppDispatcher.register(function(payload) {
  var action = payload.action;

switch(action.type) {

<span class="k">case</span> <span class="nx">ActionTypes</span><span class="p">.</span><span class="nx">CLICK_THREAD</span><span class="o">:</span>
  <span class="nx">_currentID</span> <span class="o">=</span> <span class="nx">action</span><span class="p">.</span><span class="nx">threadID</span><span class="p">;</span>
  <span class="nx">_threads</span><span class="p">[</span><span class="nx">_currentID</span><span class="p">].</span><span class="nx">lastMessage</span><span class="p">.</span><span class="nx">isRead</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
  <span class="nx">ThreadStore</span><span class="p">.</span><span class="nx">emitChange</span><span class="p">();</span>
  <span class="k">break</span><span class="p">;</span>

<span class="k">case</span> <span class="nx">ActionTypes</span><span class="p">.</span><span class="nx">RECEIVE_RAW_MESSAGES</span><span class="o">:</span>
  <span class="nx">ThreadStore</span><span class="p">.</span><span class="nx">init</span><span class="p">(</span><span class="nx">action</span><span class="p">.</span><span class="nx">rawMessages</span><span class="p">);</span>
  <span class="nx">ThreadStore</span><span class="p">.</span><span class="nx">emitChange</span><span class="p">();</span>
  <span class="k">break</span><span class="p">;</span>

<span class="k">default</span><span class="o">:</span>
  <span class="c1">// do nothing</span>

}

}); // ...

在这个例子中, ThreadStore 监听了 CLICK_THREAD 和 RECEIVE_RAW_MESSAGES Actions. 如果store没按照预想中的更新数据, 在dispatcher中注册的回调函数提供了让我们debug的空间.我们可以打出日志, 看看dispather接收了什么action和action装载的数据.

同样地, 组件中包含了一个它监听的stores的列表.

// Looking at the 'componentDidMount' will usually show
// whic stores this component listens to.
// ...
function getStateFromStores() {
  return {
    threads: ThreadStore.getAllChrono(),
    currentThreadID: ThreadStore.getCurrentID(),
    unreadCount: UnreadThreadStore.getCount()
  };
}

var ThreadSection = React.createClass({

getInitialState: function() { return getStateFromStores(); },

componentDidMount: function() { ThreadStore.addChangeListener(this.onChange); UnreadThreadStore.addChangeListener(this.onChange); },

componentWillUnmount: function() { ThreadStore.removeChangeListener(this.onChange); UnreadThreadStore.removeChangeListener(this.onChange); }, // ...

ThreadSection组件监听了ThreadStore和UnreadThreadStore发出的change事件. 我们需要保证没有其他stores会影响到这个组件的行为.

Flux把接受数据和发送数据分离了. 因此当出错的时候可以很容易地沿着数据流看看到底是哪里出错了.

相关文章:

Flux 傻瓜教程

What is the Flux Application Architecture?

官网

其他React教程:

React js Tutorial: Now is Your Time to Try It, Right in Your Browser

ReactJS教程: 开始使用Facebook的ReactJS

React Components

React Properties

React State

React.js入门教程

« 返回