ReFlux
細說
Flux作為一種應用架構(application architecture)或是設計模式(pattern),闡述的是
單向數據流
(a unidirectional data flow)的思想,並不是一個框架(framework)或者庫(library)。
前言
在細說Flux
之前,還是得提一下React ,畢竟Flux這個名字,是因為它才逐漸進入到大眾視野。
React
是facebook提出來的一個庫,用來構建用戶界面(U
ser I
nterface),它的三大特點(來自官方):
JUST THE UI
: 僅僅是一個View(components),可以認為是MVC中V
,用來構建UI界面。VIRTUAL DOM
: 虛擬dom,為的是:高性能dom渲染(利用diff算法)、組件化(向web components看齊)、多端同構(node,react native)。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為例):
當然,作為應用數據處理的模式,除了Flux,還有很多(如:傳統的MVC,MVVM),只是Flux憑借其單向數據流
特點,使得數據流變得簡單,易於調試和追蹤問題,所以更適合與React進行組合使用。
前面,我們就一直在說,Flux是一種架構,一種模式,並不是一個框架,也不是一個庫,就像我們說MVC(VM)的概念一樣,所以,遵循着Flux模式所闡述的思想自然就會出現一些庫,如:Facebook Flux
、Reflux
、Fluxxor
、Redux
等等。
本文主要講解的Reflux,不過在這之前還是需要先提一下Facebook Flux,從而為后面一些對比做一些鋪墊。
Facebook Flux
Facebook Flux,是Facebook在提出Flux架構后,給出的一個對Flux的簡單實現,可以認為是Flux庫的第一個范例,所以,也有人稱之為Original Flux
。
Facebook Flux中引入了四個概念: Dispatcher
、 Actions
、Stores
、Views(Controller-Views)
,而它們之間的關系就如同上面的那張圖所描述的一樣,構成了一個單向數據流的閉環,簡化版如下:
接下來,將以官方的TodoMVC Demo為例,來說明它們各自的作用,以及它們之間是如何配合工作的?(PS:建議讀者將源代碼clone下來,邊看邊調試)
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(訂閱者)之間卻存在着一個多對多的關系
,如下圖:
這樣的矛盾,就使得,每一個Store不得不在自己的回調函數里通過Switch語句
,來判斷當前Action的類型,來決定要不要進行處理,那么暫且拋開性能不說,顯然,這樣寫法,卻顯得繁重且不夠優雅。
於是,接下來,看看Reflux在Facebook Flux的基礎之上,做了那些優化?
Reflux
Reflux,是另一個實現Flux模式的庫,旨在使整個應用架構變得更加簡單。
准確地說,Reflux是由Facebook Flux演變而來(inspired by Facebook Flux),可以說是它的一個進化版本,自然而言就會拿兩者進行比較:詳見這里。
簡要概括一下重點,就是:
1.Reflux保留了Facebook Flux中原有的三個概念:Actions
、Stores
、Views(Controller-Views)
,去除了Dispatcher
,如果要用一張圖表示的話,就是這樣:
此時會有人問:沒有了消息中樞(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
庫。
可以用一張具體的圖來表現這一說法,如下:
從圖中可以看出,Actions
、Stores
和Views
在Reflux中分別承擔着消息發布訂閱
模式中的一個或多個角色,即:發布者(Publisher)或者 訂閱者(Subscriber/Listener),也正是基於這樣的角色扮演,才使得它能夠實現作為Flux所應該具有的單向數據流
特性(圖中紅線部分)。
總結一下:
- Reflux
單向數據流
的實現,是完全基於PubSub
設計模式的。 - Action,Store和View三者的角色分配以及分工合作,如下:
- Action 是一個
Publisher
,負責消息的分發,一般是由用戶行為(User Interaction),或是Web API觸發。 - Store 不僅是一個
Publisher
,還是一個Subscriber
(或者叫做Listener),作為Subscriber,負責監聽Action的觸發;作為Publisher,則負責通知View更新UI。 - View 是一個
Subscriber
,負責監聽Store的數據變化,做到及時更新UI。
- Action 是一個
既然Reflux中的對象不是Publisher就是Subscriber/Listener,那么代碼是如何組織的呢?
答:Reflux抽取出兩個模塊:PublisherMethods
和 ListenerMethods
,顧名思義,這兩個集合分別存儲着一個對象作為Publisher
和Listener
所應該具有的方法。
比如:
PublisherMethods中包括:trigger
、triggerAsync
等消息發布
方法。
ListenerMethods中就包括listenTo
、listenToMany
等消息訂閱
方法。
具體的細節,感興趣的同學可以看一下源碼,以及這篇文章《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消息,在觸發具體的消息之前,首先會先執行preEmit
和shouldEmit
回調。- 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: 是上述兩個方法
listen
和promise
方法的結合,做了兩件事情:消息訂閱
和異步回調
。
比如上面的例子,就可以這樣簡寫:
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.completed
和addAction.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還做了一些容錯處理,如果你不按照這個約定(即命名不規范)的話,它會這樣獲取需要注冊的回調:
以名為addItem
action為例,它的callback依次會取:
this.onAddItem -> this.addItem -> undefined(不注冊回調)
自然而然,涉及到listenTo
方法就會想起上面說的它的第三個參數defaultCallback
用來初始化,那么在listenToMany
方法對此就有這樣的約定(或者叫做映射關系):
以名為addItem
action為例(一般是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.completed
和addAction.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,它其實做了兩件事情:
- 給View添加
ListenerMethods
集合里的方法,使View具備了消息訂閱的能力。 - 在組件銷毀
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