Flux與面向組件化開發(fā)
首先要明確的是,F(xiàn)lux并不是一個前端框架,而是前端的一個設(shè)計(jì)模式纽乱,其把前端的一個交互流程簡單的模擬成了一個單向數(shù)據(jù)流。
在上圖中昆箕,我們可以看到Flux的四個核心構(gòu)成:
Action
一個交互動作鸦列,來源于用戶在頁面組件上的某個行為,如點(diǎn)擊鹏倘,失焦薯嗤,雙擊等等。其往往具有兩個組成:
* 交互類型 纤泵,例如創(chuàng)建骆姐、刪除、更新等
* 交互體,或者說交互的攜帶信息玻褪, 例如創(chuàng)建的文本
Dispatcher
分發(fā)器肉渴,從上圖的數(shù)據(jù)流中,我們可以看到带射,用戶產(chǎn)生的一個交互行為將被送入Dispatcher同规,分發(fā)器對Action進(jìn)行簡單的包裹之后分發(fā)該行為到所有 向其注冊了的Store中。
窟社!注意券勺,Dispatcher的這種廣播行為有別于Pub/Sub模型,在Pub/Sub模型中桥爽,需要聲明訂閱的消息類型朱灿,然后發(fā)布者會像訂閱者廣播特定類型的消息。而在Dispatcher中钠四,Store向其注冊的任意回調(diào)接口都不要聲明訂閱的Action類型盗扒,當(dāng)Dispatcher派發(fā)Action時,所有注冊到Dispatcher的callback都會得到相應(yīng)缀去÷略睿回調(diào)可以通過簡單工廠模式(通常是一個switch塊)來針對不對類型的Action做出不同的行為。
Store
數(shù)據(jù)存儲倉缕碎,其保存了我們某個前端App的數(shù)據(jù)褥影,并封裝了對于數(shù)據(jù)的操作。Store會向其對應(yīng)的Dispatcher注冊一個回調(diào)函數(shù)咏雌,其參數(shù)為一個交互凡怎。當(dāng)Action被派發(fā)到Store時,該回調(diào)函數(shù)被調(diào)用赊抖,借由Action中描述的交互類型统倒,Store進(jìn)行不同處理(一個簡單工廠模式),這些處理都將被持久化到Store維護(hù)的數(shù)據(jù)對象上氛雪。
Store完成數(shù)據(jù)的變更后房匆,由于Flux并不是雙向數(shù)據(jù)綁定的,所以此時报亩,頁面組件的數(shù)據(jù)并未得到更新浴鸿,組件也不會重新渲染。所以弦追,為了告知組件去更新數(shù)據(jù)岳链,Store會emit一個變更事件,并且監(jiān)聽該事件劲件。當(dāng)監(jiān)聽到變更事件產(chǎn)生時掸哑,注冊到這個事件上的回調(diào)(往往是我們App的狀態(tài)維護(hù)器的狀態(tài)更新函數(shù))會被調(diào)用左胞,從而更新各個組件的狀態(tài)。
View
顯而易見举户,這就是用戶所能看到的視圖烤宙,有別于傳統(tǒng)的MVC,在Flux中俭嘁,View并不會和數(shù)據(jù)模型(Model)產(chǎn)生交互躺枕,其只會產(chǎn)生各種交互行為(Actions),這些行為將會被送到Dispatcher中供填,如下圖所示:
TODO栗子
下面我們分析一個用React+Flux實(shí)現(xiàn)的一個Flux栗子拐云,其源碼托管在github上稠屠。
在項(xiàng)目實(shí)踐中通惫,面向組件化開發(fā)的最佳場景我認(rèn)為是 交互驅(qū)動型的開發(fā),可能描述不夠準(zhǔn)確提完,準(zhǔn)確點(diǎn)說就是一旦一個完善的交互設(shè)計(jì)稿產(chǎn)生時粘捎,我們就可以去分割和分析組件了薇缅,我們現(xiàn)在來分析Todo的交互原型:
這是交互設(shè)計(jì)師的給我們的原稿,并且攒磨,原稿可能遠(yuǎn)不止這樣一幅簡單的圖像泳桦,可能還包括更多的交互效果
我們將會把這個應(yīng)用拆分為如下組件:
TodoApp
通常,在前端面向組件化的開發(fā)過程中娩缰,我們往往需要一個頂部容器包裹住我們的組件灸撰,一個頁面可以存在若干個這樣的頂部容器,這個容器類似一個集裝箱或者盒子拼坎,封裝了某個頁面應(yīng)用的所有組件和狀態(tài)浮毯。例如,在某視頻網(wǎng)站中泰鸡,視頻播放窗口可以作為一個頂部容器债蓝,其包裹了播放窗口,進(jìn)度條鸟顺,播放選項(xiàng)等各個組件惦蚊,同時器虾,評論部分也可以作為一個頂部容器讯嫂,其包裹了評論列表,評論框等組件兆沙。
在Todo例子中欧芽,TodoApp作為一個頂部容器,包裹了所有Todo應(yīng)用需要的組件葛圃,這樣千扔,我們在應(yīng)用入口只需要渲染TodoApp就完成了整個TodoApp的渲染憎妙。但更為重要的是,TodoApp將會封裝其下各個組件需要用到的狀態(tài)曲楚,通過數(shù)據(jù)流厘唾,各個組件將會收到狀態(tài),并且在狀態(tài)改變時龙誊,重新渲染自己抚垃,最終更新頁面內(nèi)容。
Header
這是一個頭部組件趟大,根據(jù)交互設(shè)計(jì)鹤树,他除了將保有靜態(tài)的“todos”文字標(biāo)題以外,還將會具有如下行為:
- 右側(cè)輸入框失焦或者相應(yīng)回車鍵:創(chuàng)建新的任務(wù)
Footer
這是一個底部組件逊朽,它將顯示未完成任務(wù)數(shù)罕伯,并能刪除所有已完成任務(wù),故而叽讳,首先他需要獲得如下狀態(tài):
- 所有任務(wù):
- 通過遍歷任務(wù)的完成情況追他,能獲得未完成任務(wù)數(shù)
- 通過遍歷任務(wù)的完成情況,統(tǒng)計(jì)已完成任務(wù)的信息
- 如果當(dāng)前無任務(wù)岛蚤,不現(xiàn)實(shí)Footer
并且湿酸,他具有如下行為:
- 單擊右側(cè)按鈕(Clear completed): 清除所有已完成任務(wù)
MainSection
該組件將會負(fù)責(zé)渲染所有的以創(chuàng)建任務(wù),因而他需要維護(hù)的狀態(tài)為:
- 所有任務(wù)
其具有的行為:
- 點(diǎn)擊頂部左側(cè)圖標(biāo)按鈕:完成/取消完成所有任務(wù)灭美,具體根據(jù)所有任務(wù)是否都完成了決定
TodoItem
這是Todo項(xiàng)推溃,其Todo對象來源于MainSection的迭代,并且該組件具有如下行為:
- 單擊左側(cè)按鈕:完成/取消完成該任務(wù)
- 單擊右側(cè)按鈕:刪除該Todo
- 雙擊Todo文本:進(jìn)入如下的編輯模式
我們不難發(fā)現(xiàn)届腐,“是否處于編輯模式”實(shí)際上可作為該組件的一個狀態(tài)铁坎,該狀態(tài)的切花直接影響了該組件的展示和行為,所以犁苏,組件應(yīng)當(dāng)維護(hù)一個狀態(tài):
- 是否編輯模式
在編輯模式中硬萍,具有如下行為:
- 輸入框失焦或者相應(yīng)回車鍵:更新任務(wù)
可以看到,在Header組件及TodoItem組件的輸入框組件具有一致的交互行為围详,所以朴乖,我們可以將其提出來作為單獨(dú)的組件,這也體現(xiàn)了助赞,一份晚上的交互設(shè)計(jì)原型將預(yù)測到實(shí)現(xiàn)過程中的復(fù)用和抽象买羞,避免了一些代碼重構(gòu)的時間。
TodoTextInput
現(xiàn)在雹食,我們抽象出一個可復(fù)用的輸入組件TodoTextInput畜普,他具有如下行為:
- 輸入框失焦或者相應(yīng)回車鍵:調(diào)用存儲過程(創(chuàng)建,更新等等)
綜上群叶,我們以一個簡單的示意圖表示如上的劃分:
![組件結(jié)構(gòu)](http://7pulhb.com2.z0.glb.clouddn.com/%E7%BB%84%E4%BB%B6%E7%BB%93%E6%9E%84.png)
上圖藍(lán)色橢圓封裝的屬性吃挑, 黃色橢圓封裝的是狀態(tài)钝荡。 在每個TodoItem中,還需要單獨(dú)維護(hù)一個”是否可編輯狀態(tài)”舶衬,該狀態(tài)決定了TodoItem的行為和展示埠通。
注意到,因?yàn)?strong>所有任務(wù)這個狀態(tài)會被多個組件共享(MainSection逛犹,F(xiàn)ooter)植阴,所以,該狀態(tài)被提到了頂部容器TodoApp中進(jìn)行維護(hù)圾浅,這樣掠手,通過TodoApp的SetState()方法,所有綁定到TodoApp的組件都獲得了狀態(tài)更新狸捕,避免了組件間的相互引用喷鸽,實(shí)現(xiàn)了組件解耦(唯一的耦合存在于組件與頂層容器),如下圖所示:
![狀態(tài)共享](http://7pulhb.com2.z0.glb.clouddn.com/%E7%8A%B6%E6%80%81%E6%9B%B4%E6%96%B0-%E6%9D%BE%E8%80%A6%E5%90%88.png)
倘若我們在MainSection及Footer中分別維護(hù)這個狀態(tài)灸拍,由于MainSection與Footer屬于平級的組件做祝,所以,當(dāng)MainSection中的所有任務(wù)這一狀態(tài)發(fā)生改變時鸡岗,為使Footer中的狀態(tài)也發(fā)生改變混槐,為此,MainSection及Footer組件都要保存對方引用轩性,二者將會是強(qiáng)耦合的声登,如下圖所示:
![狀態(tài)不共享](http://7pulhb.com2.z0.glb.clouddn.com/%E7%8A%B6%E6%80%81%E6%9B%B4%E6%96%B0-%E5%BC%BA%E8%80%A6%E5%90%88.png)
設(shè)想,如果以后還有更多的組件需要所有任務(wù)這一狀態(tài)揣苏,這一設(shè)計(jì)模式將會是十分糟糕的悯嗓,任何一個組件的脫離將可能導(dǎo)致整個引用網(wǎng)絡(luò)的崩潰,如下圖所示:
![狀態(tài)更新-崩潰](http://7pulhb.com2.z0.glb.clouddn.com/%E7%8A%B6%E6%80%81%E6%9B%B4%E6%96%B0-%E5%B4%A9%E6%BA%83.png)
封裝
其中app.js為應(yīng)用的入口文件卸察,通常脯厨,單頁面應(yīng)用(SPA)都需要提供一個最初的文件,然后遞歸渲染DOM樹坑质。
下面合武,開始實(shí)現(xiàn)我們的邏輯,順著Flux的單向數(shù)據(jù)流涡扼,逐個分析Todo例子中的實(shí)現(xiàn)稼跳。
Dispatcher
js/AppDispatcher.js
var Dispatcher = require('flux').Dispatcher;
module.exports = new Dispatcher();
可以看到,Dispatcher的實(shí)現(xiàn)主要依賴于官方的flux提供支持壳澳。我們可以看下flux中的Dispatcher源碼岂贩,所有解說都放在代碼注釋中:
首先看到Dispatcher的構(gòu)造函數(shù):
function Dispatcher() {
_classCallCheck(this, Dispatcher);
this._callbacks = {}; // 保存向Dispatcher注冊回調(diào)函數(shù)
this._isDispatching = false; // 是否正在分派Action
this._isHandled = {}; // 已經(jīng)完成執(zhí)行的回調(diào)列表
this._isPending = {}; // 正在執(zhí)行中的回調(diào)列表
this._lastID = 1; // 回調(diào)Id的起始標(biāo)志
}
再看注冊方法register(callback),每個向Dispatcher的注冊的回調(diào)(callback)都擁有唯一Id進(jìn)行標(biāo)識:
/**
* 向Dispatcher注冊回調(diào)函數(shù),每個回調(diào)函數(shù)都有唯一id進(jìn)行標(biāo)識
* @param callback
* @returns {string} 注冊回調(diào)的id
*/
Dispatcher.prototype.register = function register(callback) {
var id = _prefix + this._lastID++;
this._callbacks[id] = callback;
return id;
};
/**
* 根據(jù)id刪除回調(diào)
*/
Dispatcher.prototype.unregister = function unregister(id) {
!this._callbacks[id] ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Dispatcher.unregister(...): `%s` does not map to a registered callback.', id) : invariant(false) : undefined;
delete this._callbacks[id];
};
執(zhí)行一個注冊了的回調(diào)函數(shù)將經(jīng)歷如下過程:
- 標(biāo)識當(dāng)前正在執(zhí)行的回調(diào)為進(jìn)行中(Pending)狀態(tài)
- 將用戶行為(payload)送回調(diào)執(zhí)行
- 執(zhí)行完成,標(biāo)識該回調(diào)已經(jīng)完成(Handled)
/**
* 執(zhí)行回調(diào)函數(shù),該過程為:
* 1. 標(biāo)識當(dāng)前正在執(zhí)行的回調(diào)為Pending狀態(tài)
* 2. 將payload送入回調(diào)執(zhí)行
* 3. 執(zhí)行完成,標(biāo)識該回調(diào)已經(jīng)完成
* @internal
*/
Dispatcher.prototype._invokeCallback = function _invokeCallback(id) {
this._isPending[id] = true;
this._callbacks[id](this._pendingPayload);
this._isHandled[id] = true;
};
派發(fā)dispatch(payload)指定的用戶行為payload到所有的callback將經(jīng)歷如下過程:
首先茫经,需要明確的是能夠進(jìn)行派發(fā)的前提是當(dāng)前Dispatcher為空閑狀態(tài)巷波,接下來
-
派發(fā)前的預(yù)處理_startDispatching()
- 初始化所有回調(diào)的狀態(tài)
- 設(shè)置當(dāng)前正在分發(fā)的payload
- 標(biāo)識當(dāng)前的Dispatcher狀態(tài)為"正在進(jìn)行派發(fā)"
根據(jù)注冊順序依次執(zhí)行回調(diào)_invokeCallback(id)
-
派發(fā)結(jié)束后的收尾工作_stopDispatching()
- 清除派發(fā)對象
- 標(biāo)識當(dāng)前的Dispatcher狀態(tài)為"結(jié)束派發(fā)"
/**
* 派發(fā)一個payload到所以已注冊的callback中
*/
Dispatcher.prototype.dispatch = function dispatch(payload) {
!!this._isDispatching ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch.') : invariant(false) : undefined;
this._startDispatching(payload);
try {
for (var id in this._callbacks) {
if (this._isPending[id]) {
continue;
}
this._invokeCallback(id);
}
} finally {
this._stopDispatching();
}
};
/**
* 分發(fā)payload前的初始化:
* 1. 初始化所有回調(diào)的狀態(tài)
* 2. 設(shè)置當(dāng)前正在分發(fā)的payload
* 3. 標(biāo)識當(dāng)前"正在進(jìn)行派發(fā)"
* @internal
*/
Dispatcher.prototype._startDispatching = function _startDispatching(payload) {
for (var id in this._callbacks) {
this._isPending[id] = false;
this._isHandled[id] = false;
}
this._pendingPayload = payload;
this._isDispatching = true;
};
/**
* 結(jié)束派發(fā)時的收尾工作
* 1. 清除派發(fā)對象
* 2. 標(biāo)識當(dāng)前"結(jié)束派發(fā)"
* @internal
*/
Dispatcher.prototype._stopDispatching = function _stopDispatching() {
delete this._pendingPayload;
this._isDispatching = false;
};
waitFor
再看Dispatcher中一個很重要的方法:waitFor(ids), 顧名思義萎津,該方法的作用是等待指定的回調(diào)的函數(shù)調(diào)用完成。因而抹镊,該方法主要保證了回調(diào)函數(shù)的執(zhí)行的順序性锉屈。
例如,在一個航班訂票系統(tǒng)中垮耳,我們首先要選擇完國家(Country)颈渊,才能選擇城市(City),所以终佛,當(dāng)一個類型為“更新選擇國家”的交互被送到CityStore所注冊的回調(diào)時俊嗽,為了保證能正確的選擇更新后國家的城市
CityStore.dispatchToken = flightDispatcher.register(function(payload) {
if (payload.actionType === 'country-update') {
/*
* 如果不執(zhí)行waitFor(),那么可同CityStore的回調(diào)先于ContryStore的回調(diào)執(zhí)行
* 此時的國家尚未更新,得到的默認(rèn)城市是錯誤的铃彰,而并不是最新的
* */
flightDispatcher.waitFor([CountryStore.dispatchToken]);
// waitFor()保證了ContryStore先響應(yīng)了'country-update'绍豁,即保證了國家更新先于城市更新
// 此時我們能正確的選擇該國家的城市
CityStore.city = getDefaultCityForCountry(CountryStore.country);
}
});
下面我們看waitFor()的源碼實(shí)現(xiàn):
/**
* 等待指定的回調(diào)完成
*/
Dispatcher.prototype.waitFor = function waitFor(ids) {
!this._isDispatching ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Dispatcher.waitFor(...): Must be invoked while dispatching.') : invariant(false) : undefined;
for (var ii = 0; ii < ids.length; ii++) {
var id = ids[ii];
if (this._isPending[id]) {
!this._isHandled[id] ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Dispatcher.waitFor(...): Circular dependency detected while ' + 'waiting for `%s`.', id) : invariant(false) : undefined;
continue;
}
!this._callbacks[id] ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Dispatcher.waitFor(...): `%s` does not map to a registered callback.', id) : invariant(false) : undefined;
this._invokeCallback(id);
}
};
Store實(shí)現(xiàn)
在js/stores/TodoStore.js中:
首先,我們維護(hù)我們的數(shù)據(jù)對象牙捉,并提供若干對于該數(shù)據(jù)的操作:
// 保存TODO列表
var _todos = {};
/**
* 創(chuàng)建一個 Todo
* @param text {string} Todo內(nèi)容
*/
function create(text) {
// ...
}
/**
* 更新一個 TODO item
* @param id {string}
* @param updates {object} 待更新對象的屬性
*/
function update(id, updates) {
// ...
}
/**
* 根據(jù)一個更新屬性值對象更新所有 Todo
* @param updates {object}
*/
function updateAll(updates) {
// ...
}
/**
* 刪除 Todo
* @param id {string}
*/
function destroy(id) {
// ...
}
/**
* 刪除所有的已完成的 TODO items
*/
function destroyCompleted() {
// ...
}
然后導(dǎo)出一個全局單例竹揍,該單例提供了常用的外部訪問接口,并且通過node提供的EventEmitter來實(shí)現(xiàn)事件的派發(fā)和監(jiān)聽:
var TodoStore = assign({}, EventEmitter.prototype, {
/**
* 是否所有TODO 都已完成
* @return {boolean}
*/
areAllComplete: function () {
// ...
},
/**
* 獲得所有的TODO
* @returns {object}
*/
getAll: function () {
// ...
},
/**
* 發(fā)送變更事件
*/
emitChange: function () {
// ...
},
/**
* 添加變更事件監(jiān)聽
* @param callback
*/
addChangeListener: function (callback) {
// 一旦受到變更事件, 觸發(fā)回調(diào)
/*
* 例如, 當(dāng)我們創(chuàng)建一條todo時,
* TodoStore將會發(fā)出一條變更事件,
* 上游的狀態(tài)維護(hù)器將會調(diào)用callback進(jìn)行狀態(tài)更新
*/
this.on(CHANGE_EVENT, callback);
},
/**
* 刪除變更事件監(jiān)聽
* @param callback
*/
removeChangeListener: function (callback) {
this.removeListener(CHANGE_EVENT, callback);
}
});
最后邪铲,我們需要向AppDispatcher注冊回調(diào)函數(shù)芬位,以便在payload被分發(fā)到TodoStore時,TodoStore能做出相應(yīng):
AppDispatcher.register(function callback(action) {
var text;
// 根據(jù)不同的action類型(即不同的交互邏輯), 執(zhí)行不同過程
switch (action.actionType) {
case TodoConstants.TODO_CREATE:
text = action.text.trim();
if( text!=='') {
create(text);
// 一旦變更,發(fā)出變更事件,
TodoStore.emitChange();
}
break;
case TodoConstants.TODO_TOGGLE_COMPLETE_ALL:
// ...
break;
case TodoConstants.TODO_UNDO_COMPLETE:
// ...
break;
case TodoConstants.TODO_COMPLETE:
// ...
break;
case TodoConstants.TODO_UPDATE_TEXT:
// ...
break;
case TodoConstants.TODO_DESTROY:
// ...
break;
case TodoConstants.TODO_DESTROY_COMPLETED:
// ...
break;
default:
// no op
}
});
!注意, 在回調(diào)執(zhí)行過程中带到,如果發(fā)生狀態(tài)的變動昧碉,需要發(fā)出變更事件,以便上游注冊的回調(diào)函數(shù)能夠獲得相應(yīng)并更新狀態(tài)到下游揽惹。
Actions
我們將TodoApp中常見的Action都封裝到了js/TodoActions.js中, 通過其中的AppDispatcher單例晌纫,我們可以將Action派發(fā)出去:
var TodoActions = {
/**
* 創(chuàng)建行為
* @param text {string}
*/
create: function (text) {
// 將創(chuàng)建行為送到Dispatcher, Dispatcher派發(fā)這個行為(action對象)到各個Store
AppDispatcher.dispatch({
actionType: TodoConstants.TODO_CREATE,
text: text
});
},
/**
* 更新行為
* @param id {string}
* @param text {string}
*/
updateText: function (id, text) {
// ...
},
/**
* 全部設(shè)置為完成
* @param todo
*/
toggleComplete: function (todo) {
// ...
},
/**
* 標(biāo)記所有的Todo為已完成
*/
toggleCompleteAll: function () {
// ...
},
/**
*
* @param id
*/
destroy: function (id) {
// ...
},
/**
* 刪除所有已完成的Todo
*/
destroyCompleted: function() {
// ...
}
};
Components
下面開始實(shí)現(xiàn)各個組件, 個人偏向的流程是先在組件目錄下創(chuàng)建好各個組件文件永丝,并以如下內(nèi)容先導(dǎo)出锹漱,亦即,我們先創(chuàng)建空白組件慕嚷,之后再依序進(jìn)行裝填
var React = require('react');
var Header = React.createClass({
render: function () {
// TODO::render
},
});
module.exports = Header;
裝填順序我會選擇先裝填頂部容器(此例中即為TodoApp)哥牍,之后按照DOM樹自底向上的進(jìn)行裝填:
TodoApp.react.js:
var Footer = require('./Footer.react');
var Header = require('./Header.react');
var MainSection = require('./MainSection.react');
var React = require('react');
var TodoStore = require('../stores/TodoStore');
// 在根DOM下維護(hù)狀態(tài),
// 這樣的狀態(tài)往往是共享狀態(tài)(會向下傳遞的狀態(tài))
function getTodoState() {
return {
allTodos: TodoStore.getAll(),
areAllComplete: TodoStore.areAllComplete()
};
}
var TodoApp = React.createClass({
getInitialState: function () {
return getTodoState();
},
/**
* 綁定生命期--掛載
*/
componentDidMount: function () {
// 掛載時再為TodoStore添加監(jiān)聽器
TodoStore.addChangeListener(this._onChange);
},
componentWillUnmount: function () {
TodoStore.removeChangeListener(this._onChange);
},
render: function () {
return (
<div>
<Header />
<MainSection
allTodos={this.state.allTodos}
areAllComplete={this.state.areAllComplete}
/>
<Footer allTodos={this.state.allTodos}/>
</div>
);
},
/**
* Event handler for 'change' events coming from the TodoStore
*/
_onChange: function() {
this.setState(getTodoState());
}
});
module.exports = TodoApp;
為了方便,TodoApp不僅維護(hù)allTodos(所有任務(wù))這個狀態(tài)喝检,還維護(hù)areAllComplete(是否所有任務(wù)都已完成)嗅辣,該狀態(tài)主要服務(wù)于MainSection中的---”完成所有/取消完成所有任務(wù)“這一用例,避免重復(fù)遍歷allTodos的開銷挠说。
我們可以看到澡谭,TodoApp提供了一個_onChange()方法作為TodoStore的change事件的回調(diào),當(dāng)TodoStore發(fā)出change事件時损俭,TodoApp將刷新狀態(tài)蛙奖,借此通知其下組件如MainSection等重新渲染潘酗。通過這樣一個頂層組件,我們不用把對Store的事件監(jiān)聽和俘獲進(jìn)行集中化處理雁仲,避免在更多的組件的中監(jiān)聽Store的事件仔夺。
更多組件的實(shí)現(xiàn)不再贅述。下面著重介紹flux的工作流程
工作流程
我們以創(chuàng)建新的Todo這一工作流程為例展示Flux的工作過程攒砖。在Flux中缸兔,該流程如下圖所示:
- 我們在創(chuàng)建Todo的輸入框中敲入數(shù)據(jù),在輸入框上吹艇,我們監(jiān)聽了失焦(onBlur)和按下鍵盤按鍵(onKeyDown)的事件
// js/components/TodoTextInput.react.js
/**
* @return {object}
*/
render: function() /*object*/ {
return (
<input
className={this.props.className}
id={this.props.id}
placeholder={this.props.placeholder}
onBlur={this._save}
onChange={this._onChange}
onKeyDown={this._onKeyDown}
value={this.state.value}
autoFocus={true}
/>
);
},
當(dāng)事件發(fā)生時惰蜜,調(diào)用_save()方法進(jìn)行處理:
_save: function() {
this.props.onSave(this.state.value);
this.setState({
value: ''
});
},
- 注意,我們通過給TodoTextInput設(shè)定onSave屬性來指定事件發(fā)生后的回調(diào)受神,在Header組件中蝎抽,我們通過屬性指定了這個回調(diào),使得我們在失焦或回車按下后路克,能夠像Dispatch請求派發(fā)(dispatch)一個“創(chuàng)建行為”
// js/components/Header.react.js
/**
* @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>
);
},
/**
* 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);
}
}
我們之所以不再TodoTextInput中創(chuàng)建Action主要是考慮到靈活性樟结,其save后的回調(diào)通過綁定onSave而不是寫死在save()中,可以派發(fā)種類更多的Action
- 在TodoActions.create()中精算,我們會請求Dispatcher派發(fā)一個Todo創(chuàng)建行為到TodoStore:
// js/actions/TodoActions.js
/**
* @param {string} text
*/
create: function(text) {
AppDispatcher.dispatch({
actionType: TodoConstants.TODO_CREATE,
text: text
});
},
- TodoStore在接收到Dispatcher派發(fā)來的Action之后瓢宦,其注冊的回調(diào)被調(diào)用, 并且在持久化這個TODO之后,引起了全局維護(hù)的_todos的改變灰羽,所以TodoStore會發(fā)射出一個change事件:
// 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;
// ...
default:
// no op
}
});
- 由于TodoApp向TodoStore注冊了一個回調(diào)監(jiān)聽change事件
// js/components/TodoApp.react.js
componentDidMount: function() {
TodoStore.addChangeListener(this._onChange);
},
此時驮履,change事件發(fā)生, 回調(diào)_onChange()被觸發(fā), TodoApp維護(hù)的狀態(tài)得到更新:
/**
* Event handler for 'change' events coming from the TodoStore
*/
_onChange: function() {
this.setState(getTodoState());
}
- 由于MainSection及Footer等組件中的屬性綁定了TodoApp維護(hù)的狀態(tài)廉嚼,所以在TodoApp刷新狀態(tài)后玫镐,二者將會重新渲染。