無標(biāo)題文章

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中供填,如下圖所示:

Action被送入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的交互原型:

Todo交互

這是交互設(shè)計(jì)師的給我們的原稿,并且攒磨,原稿可能遠(yuǎn)不止這樣一幅簡單的圖像泳桦,可能還包括更多的交互效果

我們將會把這個應(yīng)用拆分為如下組件:

TodoApp

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

TodoHeader

這是一個頭部組件趟大,根據(jù)交互設(shè)計(jì)鹤树,他除了將保有靜態(tài)的“todos”文字標(biāo)題以外,還將會具有如下行為:

  • 右側(cè)輸入框失焦或者相應(yīng)回車鍵:創(chuàng)建新的任務(wù)

Footer

TodoFooter

這是一個底部組件逊朽,它將顯示未完成任務(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

MainSection

該組件將會負(fù)責(zé)渲染所有的以創(chuàng)建任務(wù),因而他需要維護(hù)的狀態(tài)為:

  • 所有任務(wù)

其具有的行為:

  • 點(diǎn)擊頂部左側(cè)圖標(biāo)按鈕:完成/取消完成所有任務(wù)灭美,具體根據(jù)所有任務(wù)是否都完成了決定

TodoItem

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)
組件結(jié)構(gòu)
狀態(tài)維護(hù)

上圖藍(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)共享
狀態(tài)共享

倘若我們在MainSection及Footer中分別維護(hù)這個狀態(tài)灸拍,由于MainSection與Footer屬于平級的組件做祝,所以,當(dāng)MainSection中的所有任務(wù)這一狀態(tài)發(fā)生改變時鸡岗,為使Footer中的狀態(tài)也發(fā)生改變混槐,為此,MainSection及Footer組件都要保存對方引用轩性,二者將會是強(qiáng)耦合的声登,如下圖所示:

狀態(tài)不共享
狀態(tài)不共享

設(shè)想,如果以后還有更多的組件需要所有任務(wù)這一狀態(tài)揣苏,這一設(shè)計(jì)模式將會是十分糟糕的悯嗓,任何一個組件的脫離將可能導(dǎo)致整個引用網(wǎng)絡(luò)的崩潰,如下圖所示:

狀態(tài)更新-崩潰
狀態(tài)更新-崩潰

封裝

目錄結(jié)構(gòu)

其中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)歷如下過程:

  1. 標(biāo)識當(dāng)前正在執(zhí)行的回調(diào)為進(jìn)行中(Pending)狀態(tài)
  2. 將用戶行為(payload)送回調(diào)執(zhí)行
  3. 執(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)巷波,接下來

  1. 派發(fā)前的預(yù)處理_startDispatching()

    1. 初始化所有回調(diào)的狀態(tài)
    2. 設(shè)置當(dāng)前正在分發(fā)的payload
    3. 標(biāo)識當(dāng)前的Dispatcher狀態(tài)為"正在進(jìn)行派發(fā)"
  2. 根據(jù)注冊順序依次執(zhí)行回調(diào)_invokeCallback(id)

  3. 派發(fā)結(jié)束后的收尾工作_stopDispatching()

    1. 清除派發(fā)對象
    2. 標(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工作流程
  1. 我們在創(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: ''
    });
 },
  1. 注意,我們通過給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

  1. 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
    });
  },
  1. 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
  }
});
  1. 由于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());
  }
  1. 由于MainSection及Footer等組件中的屬性綁定了TodoApp維護(hù)的狀態(tài)廉嚼,所以在TodoApp刷新狀態(tài)后玫镐,二者將會重新渲染。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末怠噪,一起剝皮案震驚了整個濱河市恐似,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌傍念,老刑警劉巖矫夷,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異憋槐,居然都是意外死亡双藕,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門阳仔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來忧陪,“玉大人,你說我怎么就攤上這事∷惶” “怎么了延蟹?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長更卒。 經(jīng)常有香客問我等孵,道長稚照,這世上最難降的妖魔是什么蹂空? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮果录,結(jié)果婚禮上上枕,老公的妹妹穿的比我還像新娘。我一直安慰自己弱恒,他們只是感情好辨萍,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著返弹,像睡著了一般锈玉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上义起,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天拉背,我揣著相機(jī)與錄音,去河邊找鬼默终。 笑死椅棺,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的齐蔽。 我是一名探鬼主播两疚,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼含滴!你這毒婦竟也來了诱渤?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤谈况,失蹤者是張志新(化名)和其女友劉穎源哩,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鸦做,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡励烦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了泼诱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坛掠。...
    茶點(diǎn)故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出屉栓,到底是詐尸還是另有隱情舷蒲,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布友多,位于F島的核電站牲平,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏域滥。R本人自食惡果不足惜纵柿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望启绰。 院中可真熱鬧昂儒,春花似錦、人聲如沸委可。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽着倾。三九已至拾酝,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間卡者,已是汗流浹背蒿囤。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留虎眨,地道東北人蟋软。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像嗽桩,于是被迫代替她去往敵國和親岳守。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容

  • ##Flux與面向組件化開發(fā)首先要明確的是碌冶,F(xiàn)lux并不是一個前端框架湿痢,而是前端的一個設(shè)計(jì)模式,其把前端的一個交互...
    吳小蛆閱讀 315評論 0 0
  • 標(biāo)簽 如果要配置的標(biāo)簽扑庞,那么必須要先配置標(biāo)簽譬重,代表的包的概念。 包含的屬性 name包的名稱罐氨,要求是唯一的臀规,管理a...
    偷偷得路過閱讀 1,344評論 0 0
  • 1.要做一個盡可能流暢的ListView,你平時在工作中如何進(jìn)行優(yōu)化的栅隐? ①Item布局塔嬉,層級越少越好玩徊,使用hie...
    fozero閱讀 730評論 0 0
  • 伴隨著天天小朋友的成長恩袱,我和先生的生活重心越來越多地偏向到她這里,似乎沒有了自己的生活胶哲。 分分鐘鐘我們就變成了孩兒...
    清涼閱讀 286評論 2 8