Flux 傻瓜教程

Flux 傻瓜教程

Flux 很不直观,没什么好的文档,而且一直在更新。作为一个脑子不好使的我,真的希望在我摸索学习 Flux 的时候,有人能告诉我下面这些东西。

这是 ReactJS 傻瓜教程的续篇。

应该使用 Flux 吗?

如果你的应用需要处理动态的数据,那很可能需要使用 Flux。

如果只是一些静态的视图,它们之间不共享状态,你也没什么数据保存或者更新,就不需要使用 Flux,用了也没什么好处。

为什么需要 Flux?

Flux 是一种相对比较复杂的概念,你可能会问,为什么要增加复杂度?

90% 的 iOS 应用本质都是在把数据塞到 Table View 中。iOS 的开发工具集包含了设计优秀的 View 和 Data Model,让开发变得非常容易。

在前端(HTML、JavaScript、CSS),我们没有这些东西。但有一个很大的问题:前端项目的架构没有一个统一的标准。而且,我们已经花了很多年的时间摸索,结果好的方法没找到,出现了很多类库。使用 jQuery、Angular、Backbone 还是 Handlebars?但正真的问题是数据流的架构还没有。

什么是 Flux ?

Flux 被用来描述“单向”的数据流,且包含某些特殊的事件和监听器。Flux 没有类库,但是你需要 Flux Dispather,需要某个 JavaScript 事件库

官方文档有些意识流,作为读者不知从何开始。但是一旦你的脑子里有了 Flux 的概念,这些文档可以帮助你补充一些细节。

千万别对比 Flux 和 MVC 架构,比来比去只会越看越糊涂。

我开始深入点看看吧,我将逐个解释相关的概念。

1. View 层 “Dispatch” “actions”(分发操作)

Dispatcher 本质上就是一个事件系统。它负责广播事件和注册 callback。有且只有一个全局的 Dispatcher。推荐使用Facebook 的 Dispatcher 库,举个很简单地例子:

var AppDispatcher = new Dispatcher();

比方说有一个“新建”按钮,点击把条目添加到一个列表中。

<button onClick={ this.createNewItem }>New Item</button>

点击后会发生什么?View 层触发一个特定事件,包括事件名和新条目的数据:

createNewItem: function( evt ) {

  AppDispatcher.dispatch({
    eventName: 'new-item',
    newItem: { name: 'Marco'}    
  });

}

2. Store 响应事件

和 Flux 一样,Store 就是 Facebook 随便找的一个词。在应用中,我们需要特定的处理逻辑和数据集合来存储列表,这就是我们所谓的 Store。我们给它起个名字吧——ListStore。

Store 就是一个单例,意味着你不需要 new 出来。ListStore 就是一个全局的对象:

var ListStore = {

  items: [],

  getAll: function() {
    return this.items;
  }

};

然后 Store 响应分发出来的事件:

var ListStore = 

AppDispatcher.register( function( payload ) {

  switch( payload.eventName ) {

    case 'new-item':

      ListStore.items.push( payload.newItem );
      break;

  }

  return true;
}); 

Flux 就是使用这种传统的方法处理 callback。payload 包含一个事件名和数据。使用 switch 语句来决定调用哪个操作。

关键:Store 不是 model,而是 model 的容器。

关键:应用中唯一知道如何更新数据的就是 Store。这是 Flux 最重要的一部分。我们分发的事件是不知如何添加或者删除条目的。

再比如,应用的其他部分需要保存图片和它们的源信息,你需要使用另外一个 Store,可以取名为 ImageStore。一个 Store 就代表了应用的一个“领域”。如果应用足够大,那领域划分就是比较明显的。如果应用不大,那很可能只需要一个 Store。

只有 Store 可以注册 callback。View 永远都不应该调用 AppDispatcher.register。Dispatcher 的存在就是为了把消息从 View 传递到 Store。而 View 则是响应另外一种事件。

3. Store 触发 “change” 事件

渐入佳境了!现在数据已经变化了,我们要通知其他部分。

Store 触发一个事件,但不使用 Dispatcher。迷糊是吧,不过这就是 Flux 的方式。给 Store 插上可以触发事件的翅膀。如果你使用 MicroEvent.js ,可以这么写:

MicroEvent.mixin( ListStore );

接下来就是触发事件:

case 'new-item':

  ListStore.items.push( payload.newItem );

  ListStore.trigger( 'change' );

  break;

关键:就单单触发事件,不传递最新的条目。View 只关心是不是有东西改变了。接下来跟着看看为什么。

4. View 层响应 “change” 事件

现在我们需要显示列表了,当列表变化的时候,View 就完全重绘。对我说的没错。

首先,当 Component “上马”的时候,监听 ListStore 的 change 事件,就是在 Component 创建时:

componentDidMount: function() {  
  ListStore.bind( 'change', this.listChanged );
},

简单点,直接调用 forceUpdate,触发重绘。另外一种方式,就是把整个列表保存在 state 中。

listChanged: function() {  
  this.forceUpdate();
},

当 Component 下马时,别忘了清除监听函数,也就是在 Component 下地狱之前:

componentWillUnmount: function() {  
  ListStore.unbind( 'change', this.listChanged );
},

接下来呢?就道 Render 函数了,我故意把它放到最后来看:

render: function() {

      var items = ListStore.getAll();

      var itemHtml = items.map( function( item ) {

      return <li key={ listItem.id }>
        { listItem.name }
      </li>;

    });

    return <div>
      <ul>
          { itemHtml }
      </ul>

      <button onClick={ this.createNewItem }>New Item</button>

    </div>;
}

整个一个环就是这样——添加一个新条目,View 出发一个行为,Store 对这个行为作出相应,Store 更新,Store 触发 change 事件,接着 View 响应这个 change 事件重绘。

但是有一个问题:每次列表变化的时候都重绘整个 View,是不是有些送心病狂效率底?!

不是。

没错,render 函数确实被调用了,render 函数中所有代码都一次次的执行了。但是只有在 DOM 真正变化的时候 React 才把变化 render 出来 。render 函数生成了一个 “Virtual DOM”,React 会对比之前一次 render 出来的 DOM。如果这两次的 Virtual DOM 不一样,React 就更新真实 DOM 中变化的部分。

关键:当 Store 变化时,View 无需关心条目是添加、删除,还是修改了。它只需要整个重绘,React 的 Virtual DOM diff 算法进行复杂的运算,找出哪些真实的 DOM 节点变化了。这可以帮助你简单生活,降低血压。

该死的 “Action Creator” 是什么?

还记得吧,点击按钮,触发事件:

AppDispatcher.dispatch({  
  eventName: 'new-item',
  newItem: { name: 'Samantha' }
});

不过,如果 View 中有很多地方都需要触发这个事件,这就冗余大了。而且,所有的 View 都需要知道事件对象的特定格式。这有些别扭不是。Flux 提出一个抽象的层,叫做行为创建器,其实就是把上面的代码放到一个函数中。

ListActions = {

  add: function( item ) {
    AppDispatcher.dispatch({
      eventName: 'new-item',
      newItem: item
    });
  }

};

现在 View 只需要调用 Actions.add({name: '…'}),不用关心分发对象的语法了。

没有解决的问题

如何管理数据流是 Flux 的全部。但它无法回答下面这些问题:

  • 如何加载数据,如何把数据存储到服务端?
  • 如何让两个不相干(没有共同父节点)的组件通信?
  • 该选择哪一个 event 类库,有什么优劣取舍?
  • 为什么 Facebook 没有把所有这些作为一个类库开源出来?
  • 可以使用想 Backbone 这样的作为 Store 中存储的 model 么?

上面这几个问题的答案就是:看心情,自己玩吧!

就这么多

译者注:下面就不翻译了。

For additional resources, check out the Example Flux Application provided by Facebook. Hopefully after this article, the files in the js/ folder will be easier to understand.

The Flux Documentation contains some helpful nuggets, buried deep in layers of poorly accessible writing.

If this post helped you understand Flux, consider following me on Twitter.


原文的第一条评论同样精彩,感谢 @相杰 的翻译:

好手段!我在 Atlas 的 Facebook 工作,使用 React+Flux 的架构大概一年半了。根据我的经验做一些回答:


1、如何从服务器获取和保存数据呢?

保存数据:Action creator 会异步调用服务,并且会广播一个 success 或者 failure 的事件。然后 Store 会响应对应的 action 。你也可以在调用前广播一个 action 用来做一些更新。

加载数据:当你调用 store 去获取数据的时候,它(store)会在后台获取数据,并且在更新好对应的响应之后发送通知。Store 可以是同步的,如果数据不可取,getBlah() 会返回 null ,也可以是异步的,当数据有返回的时候,getBlah() 返回 resolved 的 Promise,这样对你来说是很 nice 的。

2、如何处理没有共同 parent 的 component 之间的通讯?

Suciu Vlad nailed this one.(没看懂)。当 Stores 和 Actions 成为全局变量的时候,他们就是不同 components 之间的交互点。

3、要使用什么样的 events 库呢?这是个问题吗?

不要在意细节。如果你使用 browserify,EventEmitter 会很好用。有一件事情你要考虑好的:你的 store 的事件广播是单一的(例如对任何事情 emit 同一个的 change 事件),还是混合的(例如 change 事件包含了哪些数据做了改变的信息,这样你可以决定忽略那些你不需要处理的事件)

4、为什么 Facebook 不把他们都打包一起发布呢?

你已经看过[1]了,这基本上就是你需要的 Flux 了。对于 NuclearMail[2],那是非常容易地使用原生 JS 去填充部件(原文:it was quite easy to just using plain old JS to fill in the pieces.)。例如,action creator 调用 dispatcher [3],或者一个 BaseStore class 封装 EventEmitter [4] 。其中一件你提到的事,你应该从 store 拉去数据,并且把数据放进 state 。我打算在这周(六个月前的吧)讲讲关于 Flux Panel 在 ReactJS 中的一些困扰(Conf),我们将会很快理清它并发布一个碉堡(awesome)的 helper ,叫 DependentStateMixin(一个粗糙的版本[5] 和一些例子[6])

facebook/flux · GitHubianobermiller/nuclearmail · GitHubnuclearmail/ThreadActions.js at master · ianobermiller/nuclearmail · GitHubnuclearmail/BaseStore.js at master · ianobermiller/nuclearmail · GitHubgithub.com/ianobermillenuclearmail/App.js at 21c4f2c6ccd7937a8be85e3bf35e47ed29f9ee91 · ianobermiller/nuclearmail · GitHub

5、我应该用类似 Backbone 的 model 作为 store 的model吗?

如果你用 Flux ,没有任何必要做这个。当数据发生改变的时候,你只需要广播一下通知就好了。你应该尽量保证你的 store 数据的稳定,与 PureRenderMixin 结合,这样会让一切很快很酷炫。我们几乎使用原生 JS 的对象和数组,并且调用 Object.freeze 去冻结那些属性(为了速度,可以在成品中设置成 Disabled 的属性)。你也可以用那些酷炫的 JS 库 github.com/facebook/imm (坑爹的有几个都打不开)


原文:Flux For Stupid People


关注微博:@前端外刊评论

或微信订阅号:

编辑于 2015-07-29 12:30