【原創】ReFlux細說


ReFlux細說

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

前言


在細說Flux之前,還是得提一下React ,畢竟Flux這個名字,是因為它才逐漸進入到大眾視野。

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

  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的語法糖可以輕易看出),也就因此有了父組件,子組件和頂層組件的概念。

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

現在,假使我們通過一個WebAPI模塊取得了數據,那么如何傳遞給React 組件(components),從而實現UI渲染呢?結合組件的層級關系,想到上述所說的第三點:自上而下的渲染,我們將數據傳遞給頂層組件(controller-view),同樣作為父組件的它,便可以通過組件的屬性(properties)將一些有用數據傳遞給它的各個子組件(各取所需數據),就這樣一級一級自上而下地傳遞下去(直到每一個葉子組件),最終,每一個組件都將得到自己渲染所需要的數據,從而完成UI的渲染。

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

有這樣一種清晰的思路:

  • 首先,我們應該需要一個數據存儲(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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM