【原創】ReFlux細說

ReFlux細說

Flux做爲一種應用架構(application architecture)或是設計模式(pattern),闡述的是單向數據流(a unidirectional data flow)的思想,並非一個框架(framework)或者庫(library)。html

前言


在細說Flux以前,仍是得提一下React ,畢竟Flux這個名字,是由於它才逐漸進入到大衆視野。node

React是facebook提出來的一個庫,用來構建用戶界面(User Interface),它的三大特色(來自官方):react

  1. JUST THE UI: 僅僅是一個View(components),能夠認爲是MVC中V,用來構建UI界面。
  2. VIRTUAL DOM : 虛擬dom,爲的是:高性能dom渲染(利用diff算法)、組件化(向web components看齊)、多端同構(node,react native)。
  3. DATA FLOW: 單向數據流(one-way data flow),指的是:一種自上而下的渲染方式(top-down rendering)。

總而言之,對於一個react web應用,它的UI將會由無數個組件(react component)嵌套組合而成,它們之間存在着層級(hierarchy)關係(經過JSX的語法糖能夠輕易看出),也就所以有了父組件,子組件和頂層組件的概念。git

然而就像上述第一點所說,React僅僅是一個View,對於一個web應用,沒有數據就顯得毫無心義。github

如今,假使咱們經過一個WebAPI模塊取得了數據,那麼如何傳遞給React 組件(components),從而實現UI渲染呢?結合組件的層級關係,想到上述所說的第三點:自上而下的渲染,咱們將數據傳遞給頂層組件(controller-view),一樣做爲父組件的它,即可以經過組件的屬性(properties)將一些有用數據傳遞給它的各個子組件(各取所需數據),就這樣一級一級自上而下地傳遞下去(直到每個葉子組件),最終,每個組件都將獲得本身渲染所須要的數據,從而完成UI的渲染。web

那麼,假若此時數據變化了(好比:對於一個列表而言,用戶點擊刪除按鈕,刪除了一條數據),咱們又該如何通知各個組件進行UI更新呢?ajax

有這樣一種清晰的思路:算法

  • 首先,咱們應該須要一個數據存儲(Store),存儲着react web應用當前的狀態(State),就像MVC中的Model同樣。
  • 而後,當用戶點擊刪除按鈕時,將會觸發一個消息(Action),告訴Store數據變化了,以及哪裏變化了(payload)。
  • 最後,Store修改了數據以後,再將新的數據傳遞給最頂層組件,從新完成一次自上而下的渲染(re-render),從而更新了UI(不要過度擔憂性能問題,VIRTUAL DOM就是用來解決這個的)。

顯然上述的幾步,React做爲一個View是不可能作到的,也正由於這樣,Flux做爲一種架構方案才被提出來,它的思想大致就是上述這幾步,經過一個單向數據的流動,完成了UI的更新,用一張圖能夠表示,以下(以Facebook Flux爲例):編程

a unidirectional data flow

固然,做爲應用數據處理的模式,除了Flux,還有不少(如:傳統的MVC,MVVM),只是Flux憑藉其單向數據流特色,使得數據流變得簡單,易於調試和追蹤問題,因此更適合與React進行組合使用。設計模式

前面,咱們就一直在說,Flux是一種架構,一種模式,並非一個框架,也不是一個庫,就像咱們說MVC(VM)的概念同樣,因此,遵循着Flux模式所闡述的思想天然就會出現一些庫,如:Facebook FluxRefluxFluxxorRedux等等。

本文主要講解的Reflux,不過在這以前仍是須要先提一下Facebook Flux,從而爲後面一些對比作一些鋪墊。

Facebook Flux


Facebook Flux,是Facebook在提出Flux架構後,給出的一個對Flux的簡單實現,能夠認爲是Flux庫的第一個範例,因此,也有人稱之爲Original Flux

Facebook Flux中引入了四個概念: DispatcherActionsStoresViews(Controller-Views),而它們之間的關係就如同上面的那張圖所描述的同樣,構成了一個單向數據流的閉環,簡化版以下:

facebook flux dataflow

接下來,將以官方的TodoMVC Demo爲例,來講明它們各自的做用,以及它們之間是如何配合工做的?(PS:建議讀者將源代碼clone下來,邊看邊調試)

Facebook Flux Demo


Views and Controller-Views

Facebook Flux中所指的Views,其實就是React Components,用做UI渲染,而相對特別的,Controller-Views指的則是頂層React Component,除了UI渲染外,它還負責接收來自Store變化的數據,並傳遞給它的Child Component(即Controller-View -> Child Views),用於子View的渲染。

在這個例子中,TodoApp就是一個Controller-View,它監聽到TodoStore的數據變化後,便會從新從TodoStore中獲取數據,而後經過調用組件setState()方法,觸發render()方法的執行,從而獲得UI的更新(自上而下的渲染)。

// 從TodoStore中獲取數據
function getTodoState() {
  return {
    allTodos: TodoStore.getAll(),
    areAllComplete: TodoStore.areAllComplete()
  };
}

var TodoApp = React.createClass({

  componentDidMount: function() {
    // TodoApp監聽TodoStore的數據變化
    TodoStore.addChangeListener(this._onChange);
  },

  render: function() {
    return (
        <div>{/* 此處代碼省去 */}</div>
    );
  },

  _onChange: function() {
    // 從新獲取TodoStore的數據,並經過調用setState,觸發re-render
    this.setState(getTodoState());
  }

});

Stores

Facebook Flux中的Stores,做爲數據存儲的模塊,相似於MVC中的Model,它負責接收Dispatcher分發過來的actions,針對不一樣的actionType,對數據就進行不一樣的操做(如:增刪改查),最後再通知View,數據變化了,須要進行UI更新。

在這個例子中,TodoStore經過變量_todos變量存儲着整個應用的數據(一個列表),並經過AppDispatcher(Dispatcher實例)註冊回調,來接收不一樣類型的Action指令,進而執行不一樣的數據操做(mutate data),最後通知TodoApp View數據改變,須要更新UI(re-render)。

// 數據存儲(一個列表)
var _todos = {};

// 操做數據的函數
function create(text) {/*此處代碼省去*/}
function update(id, updates) {/*此處代碼省去*/}
function destroy(id) {
  delete _todos[id];
}

// 接收分發過來的Action
AppDispatcher.register(function(action) {
  var text;
  
  // 判斷Action類型,採起不一樣的數據操做
  switch(action.actionType) {
  
    // 新增
    case TodoConstants.TODO_CREATE:
      text = action.text.trim();
      if (text !== '') {
        create(text);  // 建立數據,並存儲
        TodoStore.emitChange(); // 通知TodoApp數據變化,須要更新UI
      }
      break;
      
    // 更新
    case TodoConstants.TODO_UPDATE_TEXT:/*此處代碼省去*/
      break;
      
    // 刪除
    case TodoConstants.TODO_DESTROY:/*此處代碼省去*/
      break;
      
   /*此處省去部分代碼*/
  }
});

Dispatcher

Facebook Flux中,Dispatcher起到了一箇中央樞紐(Central Hub)的角色,它存儲着一張Stores列表清單,而且負責Actions的分發工做,即Action的一旦觸發,Dispatcher將會通知列表清單上的全部的Stores,每個Store則選擇性地針對該Action進行特定處理(或者不處理)。

在一個應用中,Dispatcher實例只容許有一個(Single),也就是說它將做爲一個單例而存在。

在這個例子中,AppDispatcher就是這樣一個單例,咱們在TodoStores經過AppDispatcher.register()註冊回調(見上段代碼),來接收不一樣類型的Actions(消息訂閱),在TodoActions裏經過AppDispatcher.dispatch()執行不一樣Actions的分發(消息發佈),以下:

var TodoActions = {
  // 新增Action
  create: function(text) {
    AppDispatcher.dispatch({  // 通知TodoStore對數據進行修改(帶有Action類型和關聯數據)
      actionType: TodoConstants.TODO_CREATE, // Action類型:create
      text: text  // 傳遞給TodoStore的數據
    });
  },
  // 更新Action
  updateText: function(id, text) {
    AppDispatcher.dispatch({
      actionType: TodoConstants.TODO_UPDATE_TEXT, // Action類型:update
      id: id,
      text: text
    });
  },
  // 刪除Action
  destroy: function(id) {
    AppDispatcher.dispatch({
      actionType: TodoConstants.TODO_DESTROY, // Action類型:destroy
      id: id
    });
  }
  
  /*此處省去部分代碼*/
};

Actions

Facebook Flux中的有一個概念叫作Action Creator,能夠將它理解爲一個方法(即helper method),專門用來建立某種類型的Action。

上一段代碼中,TodoActions模塊就提供了這些helper methods(或者叫作Action Creators),如:

  • TodoActions.create(text)
  • TodoActions.updateText(id, text)
  • TodoActions.destroy(id)
  • ...

上述每個方法在內部,都定義了本身的常量類型(actionType),而且將接收的參數做爲數據(payload),從而封裝成一個完整的Action(即actionType + payload = Action)。

最後,再統一經過調用Dispatcher.dispatch()將特定的Action以消息的形式分發出去(即傳遞給Stores),Stores在獲得Action後,即可以經過Action.actionType來斷定採起某種操做(或者忽略這個Action),而執行操做時須要用到的數據則來自Action.payload


思考

Facebook Flux中提出的這四個概念,承擔着各自角色,經過互相協做,造成了一個單向數據流的閉環。
------【推薦你們看下這篇文章《A cartoon guide to Flux》,生動形象地描述了這幾個角色。】

說完了Facebook Flux,讓咱們靜靜思考一下,存在的不足:

假若,有一個單頁面應用,程序中就可能存在N個store,每一個store都會監聽1~N個action,代碼就會像這樣:

// storeA.js
Dispatcher.register(function (action) {
    switch(action.actionType) {
        case 'actionA': break;
        case 'actionB': break;

        /* ... 1~N個action */
        
        case 'actionN': break;
    }
});

// storeB.js
Dispatcher.register(function () {
    // 同上
});

/* ... */
/* ... 1~N個store */
/* ... */

// storeN.js
Dispatcher.register(function () {
    // 同上
});

假使此時,觸發了一個actionX,那麼storeA~storeB的經過Dispatcher.register()註冊的回調函數會按註冊順序依次被觸發(無一例外),也就是說每一個store都會獲得actionX通知,惟一不一樣的可能就是:每一個store模塊,會經過各自的switch語句進行判斷,有的對actionX作處理,有的則不處理(忽略),那麼問題來了:

『既然有些store對actionX不須要處理,那麼它們註冊的回調執行是否有必要?畢竟是函數執行是有開銷的,若是有1000個store對actionX不"感冒"的的話,會不會很浪費資源?』

分析下這個問題:Facebook Flux是以Dispatcher(發佈者)做爲消息中樞,全部的Action消息都會統一從這裏分發出去,廣播給全部的Store(訂閱者),也就是說:發佈者(Dispatcher)和訂閱者(Stores)之間存在着一對多的關係,而事實上Actions(消息)和Stores(訂閱者)之間卻存在着一個多對多的關係,以下圖:

enter image description here

這樣的矛盾,就使得,每個Store不得不在本身的回調函數裏經過Switch語句,來判斷當前Action的類型,來決定要不要進行處理,那麼暫且拋開性能不說,顯然,這樣寫法,卻顯得繁重且不夠優雅。

因而,接下來,看看Reflux在Facebook Flux的基礎之上,作了那些優化?

Reflux


Reflux,是另外一個實現Flux模式的庫,旨在使整個應用架構變得更加簡單

準確地說,Reflux是由Facebook Flux演變而來(inspired by Facebook Flux),能夠說是它的一個進化版本,天然而言就會拿二者進行比較:詳見這裏

簡要歸納一下重點,就是:

1.Reflux保留了Facebook Flux中原有的三個概念:ActionsStoresViews(Controller-Views),去除了Dispatcher,若是要用一張圖表示的話,就是這樣:

reflux data flow

此時會有人問:沒有了消息中樞(Dispatcher),消息Actions如何發佈出去,並傳遞到Stores呢?

答:在Reflux中,每個Action自己就是一個Publisher(消息發佈者),即自帶了消息發佈功能;而每個Store除了做爲數據存儲以外,它仍是一個Subscriber,或者叫作Listener(消息訂閱者),天然就能夠經過監聽Action,來獲取到變化的數據。

2.Store之間能夠互相監聽

這樣的場景仍是有的,好比:在單頁面應用中,若是不一樣Page擁有不一樣的Store,那麼就可能會出現:子頁面Store數據變化後,須要通知到父頁面Store進行相應修改的狀況。


回顧上一節中,對於Facebook Flux的思考,所遺留的問題點,在Reflux中是否解決了呢?

答案是:確定的。

這裏先簡單說明下:

前面講到Actions和Stores(消息訂閱者)間自己就存在着多對多的關係,而做爲Publisher(消息發佈者),

  • 在Facebook Flux中只有一個,即Dispatcher,因此,不得不在消息發佈時,經過在payload中添加actionType字段來區分消息類型,且Store也所以不得不在回調函數中用Switch語句進行判斷actionType處理。

  • 而在Reflux中,因爲每個Action都是一個Publisher,且具備特定的含義(actionType),即多個Publisher對應於多個Subscriber(或叫作Listener),Store即可以有目的性地選擇訂閱想監聽的Action,而不是監聽全部的Action,再經過Switch語句進行篩選;另外,Action(消息)的發佈,也只會通知給以前有訂閱過的Store,而不是全部Store,因此並不會形成任何資源浪費。

歸結一點,就是Reflux將Dispatcher的功能合併到Action中去,使得每個Action都具備了消息發佈的功能,能夠直接被Store所監聽(即listenable)。


本質

不管是從具體的用法,仍是從源碼的架構來看,Reflux本質上能夠理解爲一個PubSub

能夠用一張具體的圖來表現這一說法,以下:

enter image description here

從圖中能夠看出,ActionsStoresViews在Reflux中分別承擔着消息發佈訂閱模式中的一個或多個角色,即:發佈者(Publisher)或者 訂閱者(Subscriber/Listener),也正是基於這樣的角色扮演,才使得它可以實現做爲Flux所應該具備的單向數據流特性(圖中紅線部分)。

總結一下:

  1. Reflux單向數據流的實現,是徹底基於PubSub設計模式的
  2. Action,Store和View三者的角色分配以及分工合做,以下:
    • Action 是一個Publisher,負責消息的分發,通常是由用戶行爲(User Interaction),或是Web API觸發。
    • Store 不只是一個Publisher,仍是一個Subscriber(或者叫作Listener),做爲Subscriber,負責監聽Action的觸發;做爲Publisher,則負責通知View更新UI。
    • View 是一個Subscriber,負責監聽Store的數據變化,作到及時更新UI。

既然Reflux中的對象不是Publisher就是Subscriber/Listener,那麼代碼是如何組織的呢?

答:Reflux抽取出兩個模塊:PublisherMethodsListenerMethods,顧名思義,這兩個集合分別存儲着一個對象做爲PublisherListener所應該具備的方法。

好比:

PublisherMethods中包括:triggertriggerAsync消息發佈方法。

ListenerMethods中就包括listenTolistenToMany消息訂閱方法。

具體的細節,感興趣的同窗能夠看一下源碼,以及這篇文章《The Reflux data flow model》詳細介紹了Reflux與PubSub的關係。


詳解

這一節的主要目的是:經過代碼示例和應用場景,儘量地講解Reflux每一個API的全貌,以及將代碼如何寫得更簡潔優雅?

Action

在Reflux中,由於沒有了Action Creator的概念,因此,Action的建立都是經過統一的API:Reflux.createAction()或者Reflux.createActions()來實現。

1.經過Reflux.createAction()建立單個Action,代碼以下:

// 擁有配置
var action = Reflux.createAction({
    actionName: 'addItem',  // 其實這個actionName並無什麼用,可不傳
    asyncResult: true,
    sync: false,
    children: ['success']
});

// 簡化
var action = Reflux.createAction('addItem')

// 或者匿名
var addItemAction = Reflux.createAction();

注意:Reflux.createAction()的返回值是一個特殊的對象 --- 函數(functor),這樣的設計實際上是爲了方便Action的觸發,顯得更加函數化編程(FRP) ,就像下面這樣使用:

addItemAction({a: 1});
action('hello world', 'Lovesueee');

action建立的時候,能夠進行參數的配置,具體的參數意義以下:

  • sync: 設置爲true,指定action的默認觸發方式爲同步
  • children: 用於建立子Action(主要是用在異步操做的時候,後面會講到)
  • asyncResult:設置爲true時,自動建立兩個名爲'completed''failed'的子Action(能夠認爲是設置子Action的一個快捷方式)

2.經過Reflux.createActions()建立多個Action,即Actions集合,代碼以下:

var actions = Reflux.createActions(['addItem', 'deleteItem']);

// 個別action配置
var actions = Reflux.createActions(['addItem', {
    deleteItem: {
        asyncResult: true,
        children: ['success'],
    },
    updateItem: {...}
}]);

// 也能夠這樣
var actions = Reflux.createActions({
    addItem: {},
    deleteItem: {
        asyncResult: true,
        children: ['success']
    },
    updateItem: {...}
});

注意:Reflux.createActions()返回的是一個普通的對象,即Actions集合,因此Action觸發時,須要指定actionName,就像這樣:

actions.addItem({...});
actions.deleteItem();

通常說來,在實際項目代碼中,因爲涉及到的Action較多,因此通常都是調用Reflux.createActions()一次性建立Actions集合,比較方便。另外,以後Store經過listenables字段與Action進行關聯時,須要的也是一個Actions集合。


以前就提到,Action做爲一個Publisher,會擁有PublisherMethods集合裏提供的一系列方法,這裏統一舉例說明:

  • listen:Action消息訂閱
var addAction = Reflux.createAction();

addAction.listen(function (url) {
    // 默認上下文this是addAction
    $.ajax(url).done(function () {
        // todo: save to store
    });
});

addAction('/xxx/add');

  • trigger 同步觸發Action消息,在觸發具體的消息以前,首先會先執行preEmitshouldEmit回調。

    • preEmit返回值(非undefined)將做爲shouldEmit函數的入參,用於修改payload
    • shouldEmit的返回值(true or false),將做爲是否真正觸發消息的標誌

舉幾個例子說明下,preEmit和shouldEmit的使用,以下:

preEmit用於異步請求,下面兩種方法是等價的:

var actions = Reflux.createActions({
    add: {
        asyncResult: true,
        preEmit: function (url) {
            $.ajax(url)
                .done(this.completed)
                .fail(this.failed);
        }
    }
});

// 等價於

var actions = Reflux.createActions({
    add: {
        asyncResult: true
    }
});

actions.add.listen(function(url) {
    $.ajax(url)
        .done(this.completed)
        .fail(this.failed)
});

preEmit用於修改payload

var actions = Reflux.createActions(['takePhoto']);

// 映射
var maps = {
    'photo': {
        maxSize: 1000     // 從相冊獲取
    },
    'camera': {           // 拍照
        maxSize: 2000,
        maxSelect: 10
    }
};

actions.takePhoto.preEmit = function (type) {
    return maps[type] || maps['photo'];
};

actions.takePhoto.listen(function (options) {
    // do ajax
    console.log(options);
});

actions.takePhoto('photo');
// 或者
// actions.takePhoto('camera');

shouldEmit的使用(防止action的頻繁觸發)

var requesting = false;
var actions = Reflux.createActions(['submit']);

actions.submit.shouldEmit = function () {
    return !requesting;
}

actions.submit.listen(function (url) {

    requesting = true;
    
    $.ajax(url).done(function () {
        // success
    }).fail(function () {
        // error
    }).always(function () {
        requesting = false;
    });
});

// 點擊按鈕
$('#btn').click(function () {
    actions.submit('url/submit');
});

  • promise: 語法糖,用於簡寫異步Action,下面兩種方法是等價的:
var addAction = Reflux.createAction({
    children: ['completed', 'failed'] // 等價於 asyncResult: true
});

addAction.listen(function (url) {
    var me = this;
    $.ajax(url).done(function (data) {
        me.completed(data);
    }).fail(function () {
        me.failed();
    });
});

// 等價於
addAction.listen(function (url) {
    this.promise($.ajax(url));
});

addAction('/url/add');

  • listenAndPromise: 是上述兩個方法listenpromise方法的結合,作了兩件事情:消息訂閱異步回調

好比上面的例子,就能夠這樣簡寫:

addAction.listenAndPromise(function(url) {
    return $.ajax(url);    // 注意:返回promise對象
});

  • triggerAsync: 異步觸發Action消息(而trigger同步觸發消息),相似於setTimeout(function () {action();}, 0)

  • triggerPromise 觸發Action消息,能夠經過返回的promise將異步請求的數據直接帶回,而不須要通過Store。

改寫上面的例子,以下:

var addAction = Reflux.createAction({
    asyncResult: true
});

addAction.listenAndPromise(function(url) {
    return $.ajax(url);    // 注意:返回promise對象
});

// 觸發消息,監聽異步子action的成功與失敗
// action這裏能夠獲取到數據,
addAction.triggerPromise('/url/add').then(function (data) {
    console.log(data);
}, function () {
    console.log('failed');
});

最後再說說,子Action的概念,其實以前都用到了,主要是用於異步請求,成功和失敗回調的執行,這裏簡單說明一下:

在利用Reflux.createAction建立Action之初,能夠經過下面的兩種方式建立子Action:

var addAction = Reflux.createAction({
    asyncResult: true
});

// 等價於

var addAction = Reflux.createAction({
    children: ['completed', 'failed']
});

在建立以後這兩個子Action在數據存儲結構中,即可以直接經過addAction.completedaddAction.failed訪問。


Store

Store做爲數據存儲中心,且由於介於Actions和Views之間,因此同時承擔着Publisher(消息發佈者)和Subscriber(消息訂閱者)兩種角色。

Reflux中,Store的建立一樣是經過提供的API:Reflux.createStore(),就像下面這樣:

var action = Reflux.createAction();

var store = Reflux.createStore({
    init: function () {
        // 存儲數據
        this.data = {};
            
        // Action監聽
        this.listenTo(action, this._onAction);
        // 或者
        // this.listenTo(action, '_onAction');
        // 或者
        // action.listen(this._onAction);
    },
    
    _onAction: function (msg) {
        console.log(msg);
    }
});

action('hello world');  // 觸發動做

不一樣於Action,Store返回的是一個普通的對象,一般咱們會在init方法中進行數據的存儲Action的監聽


在建立Store時,咱們能夠經過傳遞一個特殊的字段mixins,它的功能就有點相似於React Component中的mixins。

在mixin中,對於幾個特殊方法:init, preEmit, shouldEmit會進行特殊處理(組合),保證mixins裏面的方法都會被執行而,對於其餘自定義方法,有必定的覆蓋規則,好比,下面的例子中myMethod方法的覆蓋優先級就是:store > mixin3 > mixin2 > mixin。

var mixin = {
    init: function () {
        console.log('mixin:init')
    },
    myMethod: function () {
        console.log('mixin.myMethod');
    }
};

var mixin2 = {
    init: function () {
        console.log('mixin2:init')
    },
    myMethod: function () {
        console.log('mixin2.myMethod');
    }
};

var mixin3 = {
    mixins: [mixin2],
    init: function () {
        console.log('mixin3:init')
    },
    myMethod: function () {
        console.log('mixin3.myMethod');
    },
    otherMethod: function () {
        console.log('mixin3.otherMethod');
    }
};

var store = Reflux.createStore({
    mixins: [mixin, mixin3],
    init: function () {
        console.log('store:init');
    },
    myMethod: function () {
        console.log('store:myMethod');
    }
});

store.myMethod();

// mixin:init
// mixin2:init
// mixin3:init
// store:init
// store:myMethod

再從PubSub的角度說說Store:

做爲消息的發佈者,擁有着和Action同樣的能力,即擁有PublisherMethods集合的全部方法;同時做爲消息的訂閱者,用來監聽Action的觸發(或其餘Store的改變),從而改變自身數據,Store還擁有ListenerMehthods集合提供的方法。

這裏重點說一下,Store做爲消息訂閱者這個角色,擁有的幾個比較重要的方法:


  • listenTo: 監聽指定的listenable的變化,從而執行回調(這裏的listenable能夠是Action,也能夠是Store)
    (注意:reflux中,Store之間是能夠監聽的,可是不能夠互相監聽哦,避免死循環(circular loop))

舉例幾個例子,說明:

Store監聽Action

var addAction = Reflux.createAction('add');

var store = Reflux.createStore({
    init: function () {
        this.data = {
            flag: false
        };
    },
    getInitialState: function () {
        return this.data;
    }
});

store.listenTo(addAction, function (flag) {
    this.data.flag = flag;
});

addAction(true);

Store監聽其餘Store(設置listenTo第三個回調,經過調用被監聽Store的getInitialState方法獲取其初始值)

var storeA = Reflux.createStore({
    init: function () {
        this.data = {
            a: 1
        };
    },
    getInitialState: function () {
        return this.data;
    }
});

var storeB = Reflux.createStore({
    init: function () {
        this.data = {
            b: 2
        };
    }
});

storeB.listenTo(storeA, function (a) {
    this.data.a = a;
}, function (data) {
    // storeB獲取storeA的初始值
    this.data.a = data.a;
});

console.log(storeB); // storeB.data => {a: 1, b: 2}

storeA.trigger(3);

console.log(storeB); // storeB.data => {a: 3, b: 2}

  • listenToMany: 監聽指定的listenables(對象集合)變化,從而執行對應的回調(這裏的listenables是一個對象,它的每個值能夠是action,也能夠是store)

一般會這樣使用:

var actions = Reflux.createActions(['addItem', 'deleteItem']);

var store = Reflux.createStore({
    init: function () {
        this.items = [];
        this.listenToMany(actions);
    },
    onAddItem: function (item) {
        this.items.push(item);
    },
    onDeleteItem: function (item) {
        var items = this.items;
        
        items.forEach(function (val, index) {
            if (val === item) {
                items.splice(index, 1);
                // todo: break
            }
        });
    }
});

actions.addItem(1);
actions.addItem(2);

console.log(store); // store.items => [1, 2]

actions.deleteItem(1);

console.log(store); // store.items => [2]

當一個store監聽listenables對象集合(即多個監聽對象,好比:多個action)時,實際上作的事情也仍是單個消息訂閱store.listenTo(actionName, onActionName),可是這裏有一個約定(或者叫作映射關係),以上面的兩個action爲例:

actionName onActionName
addItem onAddItem
deleteItem onDeleteItem

actionName 對應的回調就是 on + actionName(駝峯寫法)

而後Reflux還作了一些容錯處理,若是你不按照這個約定(即命名不規範)的話,它會這樣獲取須要註冊的回調:

以名爲addItemaction爲例,它的callback依次會取:

this.onAddItem -> this.addItem -> undefined(不註冊回調)

天然而然,涉及到listenTo方法就會想起上面說的它的第三個參數defaultCallback用來初始化,那麼在listenToMany方法對此就有這樣的約定(或者叫作映射關係):

以名爲addItemaction爲例(通常是store之間纔會使用,且不多使用),它的defaultCallback依次會取:

this.onAddItemDefault -> this.addItemDefault -> undefined(沒有初始化回調)

這裏還須要再提起一次,子Action的概念,對於下面這段代碼:

以前會這樣作:

var addAction = Reflux.createAction({
    asyncResult: true
});

var store = Reflux.createStore({
    init: function () {
        this.listenTo(addAction.completed, 'onAddCompleted');
        this.listenTo(addAction.failed, 'onAddFailed');
    },

    onAddCompleted: function (data) {
        console.log('completed: ', data);
    },
    
    onAddFailed: function () {
        console.log('failed')
    }
});

若是用listenToMany方法來作的話,就能夠這樣簡化:

var addAction = Reflux.createAction({
    asyncResult: true
});

var store = Reflux.createStore({
    init: function () {
        this.listenToMany({add: addAction}); // 注意:參數是一個對象
    },

    onAddCompleted: function (data) {
        console.log('completed: ', data);
    },
    
    onAddFailed: function () {
        console.log('failed')
    }
});

也就是說,listenToMany方法,不但關聯了action,還會關聯它的子action,即addAction.completedaddAction.failed,這裏就又有一個約定(或者叫作映射關係):

actionName onActionName childActionName onChildActionName
add onAdd addCompleted / addFailed onAddCompleted / onAddFailed

即:on + 主action名 + 子action名(駝峯)


然而,在利用Reflux.createStore()建立之初,咱們能夠利用更簡潔的一種方式,對Store和Actions進行關聯。

以前是這樣:

var actions = Reflux.createActions(['addItem', 'deleteItem']);

var store = Reflux.createStore({
    init: function () {
        this.listenToMany(actions); // 關聯actions
    },
    onAddItem: function () {
        // todo: add
    },
    onDeleteItem: function () {
        // todo: delete
    }
});

如今能夠經過listenables字段來關聯:

var actions = Reflux.createActions(['addItem', 'deleteItem']);

var store = Reflux.createStore({
    listenables: actions  // 關聯actions
    init: function () {
        // init
    },
    onAddItem: function () {
        // todo: add
    },
    onDeleteItem: function () {
        // todo: delete
    }
});

這是一種快捷方式,其實內部原理就是store在建立的時候,調用了listenToMany方法。

注意:listenables這裏能夠是actions組成的數組,如:[actions1, actions2],就至關於多調用幾回listenToMany方法,如:

this.listenToMany(actions1); 
this.listenToMany(actions2);

View

對於View,只需在React Component裏的生命週期函數裏,負責監聽Store的變化,並及時經過調用setState()方法更新UI便可,就像下面這樣:

var myStore = Reflux.createStore({
    init: function () {
        // init
    }
});

class MyComponent extends React.Component {
    
    componentDidMount() {
        this.unsubscribe = myStore.listen(this.onChange);
    }
    componentWillUnmount: function() {
        this.unsubscribe(); // 注意:在組件銷燬時,必定要解除監聽
    }
    onChange(data) {
        this.setState(data); // re-render
    }
}

上述方式,是經過myStore.listen()來進行消息訂閱的,而實際上,View自己並無消息訂閱的能力,因此Reflux提供了一個mixin,叫作Reflux.ListenerMixin

它的實現是這樣的:

module.exports = _.extend({
    componentWillUnmount: ListenerMethods.stopListeningToAll
}, ListenerMethods);

做爲React Component的一個mixin,它其實作了兩件事情:

  1. 給View添加ListenerMethods集合裏的方法,使View具有了消息訂閱的能力。
  2. 在組件銷燬componentWillUnmount生命週期方法裏,對以前監聽的Action自動解綁。

因此,上述代碼能夠簡化爲:

import Reflux from 'reflux';
import ReactMixin from 'react-mixin';

class MyComponent extends React.Component {
    
    componentDidMount() {
        this.listenTo(myStore, this.onChange); // View自己具有了訂閱的能力
    }
    componentWillUnmount: function() {
       // nothing 無需手動解除監聽
    }
    onChange(data) {
        this.setState(data); // re-render
    }
}

// ES6 mixin寫法
ReactMixin.onClass(MyComponent, Reflux.ListenerMixin);

然而還有更簡單的寫法,就是經過Reflux.connect()來寫,以下:

import Reflux from 'reflux';
import ReactMixin from 'react-mixin';

class MyComponent extends React.Component {
    
    componentDidMount() {
        // nothing 無需手動監聽
    }
    componentWillUnmount: function() {
       // nothing 無需手動解除監聽
    }
    onChange(data) {
       // noting 無需手動setState
    }
}

// ES6 mixin寫法
ReactMixin.onClass(MyComponent, Reflux.connect(myStore));

原理是這樣的,React.connect(myStore)返回的一個mixin,這個mixin內部在作了相似下面的事情:

this.listenTo(myStore, (data) => {
    this.setState(data);
});

因此,這才幫咱們省去了手動監聽手動刪除監聽,還有手動觸發UI更新這三步。

最後


以上就是本人對Flux以及Reflux的一些理解和使用小結,不對的地方還請指出。

原創文章,轉載請說明出處:http://www.cnblogs.com/lovesueee/p/4893218.html

參考資料


  1. Flux inspired libraries with React
  2. A cartoon guide to Flux
  3. Deconstructing ReactJS's Flux
  4. The Reflux data flow model
相關文章
相關標籤/搜索