第三章 從Flux到Redux(Flux篇)

? ? ? ? 在前一章中我們已經(jīng)感受到完全用React來管理應(yīng)用數(shù)據(jù)的麻煩奴迅,在這一章中霎冯,我們將介紹Redux這種管理應(yīng)用狀態(tài)的框架,包含以下的內(nèi)部:

? ? ? ? 單項(xiàng)數(shù)據(jù)流框架的始祖Flux

? ? ? ? Flux理念的一個(gè)更強(qiáng)實(shí)現(xiàn)Redux猿棉;

? ? ? ? 結(jié)合React和Redux节吮。


3.1? Flux

? ? ? ? 要了解Redux,首先要從Flux說起瘪匿,可以認(rèn)為Redux是Flux思想的另一種實(shí)現(xiàn)方式跛梗,通過了解Flux,可以認(rèn)為Redux是Flux思想的另一種實(shí)現(xiàn)方式棋弥,通過了解Flux核偿,我們可以知道Flux一族框架(其中包括(Redux)貫徹的最重要的觀點(diǎn)--單向數(shù)據(jù)流,更重要的事顽染,我們可以發(fā)現(xiàn)Flux框架的缺點(diǎn)漾岳,從而深刻地認(rèn)識(shí)到Redux相對(duì)Flux的改進(jìn)之處轰绵。

讓我們來看看Flux的歷史,實(shí)際上尼荆,F(xiàn)lux是和React同時(shí)面世的左腔,在2013年,F(xiàn)acebook公司讓React亮相的同時(shí)耀找,也推出了Flux框架翔悠,React和Flux相輔相成,F(xiàn)acebook認(rèn)為兩者結(jié)合起來才能構(gòu)建大型的JavaScript應(yīng)用野芒。

做一個(gè)容易理解的對(duì)比蓄愁,React是用來替換jQuery的,那么Flux就是以替換Backbone.js狞悲、Ember.js等MVC一族框架為目的撮抓。

在MVC的世界里,React相當(dāng)于V(也就是View)的部分摇锋,只涉及頁(yè)面的渲染丹拯,一旦涉及應(yīng)用的數(shù)據(jù)管理部分,還是交給Model和Controller荸恕,不過乖酬,F(xiàn)lux并不是一個(gè)MVC框架,事實(shí)上融求,F(xiàn)lux認(rèn)為MVC框架存在很大問題咬像,它推翻了MVC框架,并用一個(gè)新的思維來管理數(shù)據(jù)流轉(zhuǎn)生宛。

3.1.1 MVC框架的缺陷

MVC框架是業(yè)界廣泛接受的一種前端應(yīng)用框架類型县昂,這種框架把應(yīng)用分為三個(gè)部分:

Model(模型)負(fù)責(zé)管理數(shù)據(jù),大部分業(yè)務(wù)邏輯也應(yīng)該放在Model中陷舅;

View(視圖)負(fù)責(zé)渲染用戶界面倒彰,應(yīng)該避免在View中涉及業(yè)務(wù)邏輯;

Controller(控制器)負(fù)責(zé)接受用戶輸入莱睁,根據(jù)用戶輸入調(diào)用對(duì)應(yīng)的Model部分邏輯待讳,把產(chǎn)生的數(shù)據(jù)結(jié)果交給View部分,讓View渲染出必要的輸出仰剿。

這樣的邏輯劃分耙箍,實(shí)質(zhì)上把以一個(gè)應(yīng)用劃分為多個(gè)組件一樣,就是“分而治之”酥馍。毫無疑問,相比把業(yè)務(wù)邏輯和界面渲染混在一起阅酪,MVC框架要先進(jìn)的多旨袒。這種方式得到了廣發(fā)認(rèn)可汁针,F(xiàn)acebook最初也是用這種框架。

但是砚尽,F(xiàn)acebook的工程部門逐漸發(fā)現(xiàn)施无,對(duì)于巨大的代碼庫(kù)和龐大的組織,MVC很快變的非常復(fù)實(shí)際的框架實(shí)現(xiàn)中必孤,總是允許View和Model可以直接通信猾骡,而在MVC中直接讓其對(duì)話是一種災(zāi)難。

有這么一個(gè)有意思的現(xiàn)象:凡是只在服務(wù)器端使用過MVC框架的朋友敷搪,就很容易理解和接受Flux兴想。而對(duì)于已有很多瀏覽器端MVC框架經(jīng)驗(yàn)的朋友,理解MVC和Flux要費(fèi)點(diǎn)勁赡勘。

這個(gè)原因就是:服務(wù)器端MVC往往就是每個(gè)請(qǐng)求就只在Controller-Model-View三者之間走一圈嫂便,結(jié)果就返回給瀏覽器去渲染或者其他處理了。然后這個(gè)生命周期的Controller-Model-View就可以回收銷毀了闸与,這是一個(gè)嚴(yán)格意義的單向數(shù)據(jù)流毙替;對(duì)于瀏覽器MVC框架,存在用戶的交互處理践樱,界面渲染出來后厂画,Model和View依然存在于瀏覽器中,這時(shí)候就會(huì)誘發(fā)開發(fā)者為了簡(jiǎn)便拷邢,讓現(xiàn)存的Model和View直接對(duì)話袱院。

對(duì)于MVC框架,為了讓數(shù)據(jù)流可控解孙,Controller應(yīng)該是中心坑填,當(dāng)View要傳遞信息給Model時(shí),應(yīng)該調(diào)用Controller的方法弛姜,同樣脐瑰,當(dāng)Model要更新View時(shí),也應(yīng)該通過Controller引發(fā)新的渲染廷臼。

當(dāng)Facebook推出Flux時(shí)苍在,招致了很多質(zhì)疑。很多人說荠商,F(xiàn)lux不過是對(duì)數(shù)據(jù)流管理更加嚴(yán)格的MVVC框架而已寂恬,這種說明不準(zhǔn)確,但一定意義說出了Flux的一個(gè)特點(diǎn):更嚴(yán)格的數(shù)據(jù)流控制莱没。

一個(gè)Flux應(yīng)該包含四個(gè)部分初肉,我們先粗略了解一下:

①Dispatcher,處理動(dòng)作分發(fā)饰躲,維持Store之間的依賴關(guān)系牙咏。

②Store臼隔,負(fù)責(zé)存儲(chǔ)數(shù)據(jù)處理數(shù)據(jù)相關(guān)邏輯。

③Action妄壶,驅(qū)動(dòng)Dispatcher的JavaScript對(duì)象摔握。

④View,視圖部分丁寄,負(fù)責(zé)顯示用戶界面氨淌。

若非要將MVC與Flux做個(gè)結(jié)構(gòu)對(duì)比:那么,F(xiàn)lux的Dispatcher相當(dāng)于MVC的Controller伊磺,F(xiàn)lux的Store相當(dāng)于MVC的Model盛正,F(xiàn)lux的View對(duì)應(yīng)MVC的View,至于多出來的Action奢浑,可以理解為對(duì)應(yīng)給MVC框架的用戶請(qǐng)求蛮艰。

在MVC框架中,系統(tǒng)能夠提供什么樣的服務(wù)雀彼,通過Controller暴露函數(shù)來實(shí)現(xiàn)壤蚜。每增加一個(gè)功能,Controller就要增加一個(gè)函數(shù)徊哑;在Flux的世界里袜刷,新增加功能并不需要Dispatcher增加新的函數(shù),實(shí)際上莺丑,Dispatcher自始至終只需要暴露一個(gè)函數(shù)Dispatch著蟹,當(dāng)需要增加新的功能時(shí),要做的事增加一種新的Action類型梢莽,Dispatcher的對(duì)外接口并不用改變萧豆。

我們已基本了解Flux是怎么一回事,那么接下來實(shí)踐一下看看怎么用Flux改進(jìn)一下我們的React應(yīng)用昏名。

3.1.2 Flux應(yīng)用

因?yàn)镽edux其實(shí)與Flux一脈相承涮雷,從Flux例子入手,在理解Redux的時(shí)候就會(huì)感覺非常順暢了轻局。

為了理解Flux洪鸭,首先通過命令行在項(xiàng)目目錄下安裝Flux。

npm install --save flux

利用Flux來實(shí)現(xiàn)ControlPanel應(yīng)用的相關(guān)代碼在-https://giyhub.com/mocheng/react-and-redux/tree/master/chapter-03/flux-上可以找到仑扑。最終頁(yè)面應(yīng)用效果和第二章完全 一樣览爵,通過不同實(shí)現(xiàn)方式,來體會(huì)每個(gè)方式的優(yōu)劣镇饮。

1.Dispatcher

首先蜓竹,創(chuàng)造一個(gè)Dispatcher,幾乎所有應(yīng)用都只需要擁有一個(gè)Dispatcher,本例也不例外梅肤。在src/AppDispatcher.js中司蔬,我們創(chuàng)建這個(gè)唯一Dispatcher對(duì)象:

import? {Dispatcher } from 'flux';

export default new Dispatcher();

非常簡(jiǎn)單,我們引入flux庫(kù)中的Dispatcher類姨蝴,然后創(chuàng)造一個(gè)新的對(duì)象作為這個(gè)文件的默認(rèn)輸出就足夠了。在其他代碼中肺缕,將會(huì)引用這個(gè)全局唯一飛Dispatcher對(duì)象左医。

Dispatcher存在的作用,就是用來派發(fā)action同木,接下來我們來定義應(yīng)用中涉及的action浮梢。

2.action

action顧名思義代表一個(gè)“動(dòng)作“,不過這個(gè) 動(dòng)作只是一個(gè)普通的JavaScript對(duì)象代表一個(gè)動(dòng)作的純數(shù)據(jù)彤路,類似DOM API中的事件(event)秕硝。甚至,和事件相比洲尊,action其實(shí)還是更加純粹的數(shù)據(jù)對(duì)象远豺,因?yàn)槭录€包含一些方法,但是action對(duì)象不自帶方法坞嘀,就是純粹的數(shù)據(jù)躯护。

作為管理,action對(duì)象必須有一個(gè)名為type的字段丽涩,代表這個(gè)action對(duì)象的類型棺滞,為了記錄日志和debug方便,這個(gè)type應(yīng)該是字符串類型矢渊。

定義action通常需要兩個(gè)文件继准,一個(gè)定義action類型,一個(gè)定義action的構(gòu)造函數(shù)(也稱為action creator)矮男。分成兩個(gè)文件的主要原因是在Store中會(huì)根據(jù) action類型做不同操作移必,也就有單獨(dú)導(dǎo)入action類型的需要。

在src/ActionTypes.js中昂灵,我們定義action的類型:

export? const INCREMENT ='increment';

export? const DECREMENT ='decrement';

在這個(gè)例子中避凝,用戶只能做兩個(gè)動(dòng)作,一個(gè)是點(diǎn)擊“+”按鈕眨补,一個(gè)是點(diǎn)擊"-"按鈕管削,所以我們只有兩個(gè)action類型INCREMENT和DECREMENT。

現(xiàn)在我們?cè)趕rc/Actions.js文件中定義action構(gòu)造函數(shù):

import * as ActionTypes from './ActionTypes';

import AppDispatcher from './AppDispatcher.js';

export const increment = (counterCaption)=>{

AppDispatcher.dispatcher({

type:ActionTypes.INCREMENT,

counterCaption:counterCaption

});

};

export const decrement = (counterCaption)=>{

AppDispatcher.dispatcher({

type:ActionTypes.DECREMENT,

counterCaption:counterCaption

});

};

這個(gè)Action.js導(dǎo)出了兩個(gè)action構(gòu)造函數(shù)increment和decrement撑螺,當(dāng)這兩個(gè)函數(shù)被調(diào)用的時(shí)候含思,創(chuàng)造了對(duì)應(yīng)的action對(duì)象,并立即通過AppDispatcher.dispatcher函數(shù)派發(fā)出去。

派發(fā)出去的action對(duì)象最后怎么樣了呢含潘?在下面關(guān)于Store的部分可以看到饲做。

3.Store

一個(gè)Store也是一個(gè)對(duì)象,這個(gè)對(duì)象存儲(chǔ)應(yīng)用狀態(tài)遏弱,同時(shí)還要接受Dispatcher派發(fā)的動(dòng)作盆均,根據(jù)動(dòng)作來決定是否更新應(yīng)用狀態(tài)。

接下來創(chuàng)造Store相關(guān)代碼漱逸,便于代碼維護(hù)泪姨,我們?cè)趕rc下創(chuàng)建一個(gè)子目錄stores,在這個(gè)目錄放置所有Store代碼饰抒。

在前面章節(jié)的ControlPanel應(yīng)用例子里肮砾,有三個(gè)Counter組件,還有一個(gè)統(tǒng)計(jì)三個(gè)Counter計(jì)數(shù)值之和的功能袋坑,我們遇到的麻煩就是這兩者之間的狀態(tài)如何同步的問題仗处,現(xiàn)在,我們創(chuàng)造兩個(gè)Store枣宫,一個(gè)是為Counter組件服務(wù)的CounterStore婆誓,另一個(gè)就是為總數(shù)服務(wù)的SummaryStore。

src/store/CounterStore.js,代碼如下:

const counterValues = {

'First':0,

'Second':10,

'Third':30

}

const CounterStore = Object.assign({},EventEmitter.prototype,{

getCounterVlues:function(){

return counterValues;

},

emitChange : function(){

this.emit(CHANNGE_EVENT);

},

addChangeListener:function(callback){

this.on(CHANGE_EVENT,callback);

},

removeChangeListener:function(callback){

this.removeListener(CHANGE_EVENT,callback);

}

});

當(dāng)Store狀態(tài)發(fā)生變化的時(shí)候镶柱,需要通知應(yīng)用的其他部分做必要的響應(yīng)旷档。在我們的應(yīng)用中,做出響應(yīng)的部分當(dāng)然就是View部分歇拆,但是我們不應(yīng)該硬編碼這種聯(lián)系鞋屈,應(yīng)該用消息的方式建立Store和View的聯(lián)系。這就是為什么我們用CounterStore擴(kuò)展了EventEmitter.prototype故觅,等于讓CounterStore成了EventEmitter對(duì)象厂庇,一個(gè)EventEmitte實(shí)例對(duì)象支持下列相關(guān)函數(shù)。

①emit函數(shù)输吏,可以廣播一個(gè)特定事件权旷,第一個(gè)參數(shù)是字符串類型的事件名稱。

②on函數(shù)贯溅,可以增加一個(gè)掛在這個(gè)EventEmitter對(duì)象特定事件上的處理函數(shù)拄氯,第一個(gè)參數(shù)是字符串類型的事件名稱,第二個(gè)參數(shù)是處理函數(shù)它浅。

③removeListener函數(shù)译柏,和on函數(shù)做的事情相反,刪除掛在這個(gè)EventEmitter對(duì)象特定事件上的處理函數(shù)姐霍,和on函數(shù)一樣鄙麦,第一個(gè)參數(shù)是事件名稱典唇,第二個(gè)參數(shù)是處理函數(shù)。要注意胯府,如果要調(diào)用removeListener函數(shù)介衔,就一定要保留對(duì)處理函數(shù)的引用。

對(duì)于CounterStorer對(duì)象骂因,emitChange炎咖、addChangeListener和removeChangeListener函數(shù)就是利用EvenetEmitter上述的三個(gè)函數(shù)完成對(duì)CounterStore狀態(tài)更新的廣播、添加監(jiān)聽函數(shù)和刪除監(jiān)聽函數(shù)等操作寒波。

CounterStore函數(shù)還提供了一個(gè)getCounterValues函數(shù)塘装,用于讓應(yīng)用中其他模塊可以讀取當(dāng)前的計(jì)數(shù)值,當(dāng)前的計(jì)數(shù)值存儲(chǔ)在文件模塊級(jí)的變量counterValues中影所。

上面實(shí)現(xiàn)的Store只有注冊(cè)到Dispatcher實(shí)例上才能真正發(fā)揮作用,所以還需要增加下列代碼:

import AppDispatcher from '../AppDispatcher.js';

CounterStore.dispatcherToken =AppDispatcher.register((action) =>{

if(action.type ===ActionTypes.INCREMENT){

CounterValues[action.counterCaption]++;

CounterStore.emitChange();

}else if(action.type ===ActionTypes.DECREMENT){

CounterValues[action.counterCaption]--;

CounterStore.emitChange();

}

});

這是最重要的一個(gè)步驟僚碎,要把CounterStore注冊(cè)到全局唯一的Dispatcher上去猴娩。Dispatcher有一個(gè)函數(shù)叫做register,接受一個(gè)回調(diào)函數(shù)作為參數(shù)勺阐。返回值是一個(gè)token卷中,這個(gè)token可以用于Store之間的同步,我們?cè)贑ounterStore中還用不上這個(gè)返回值渊抽,在稍后的SummaryStore中會(huì)用到蟆豫,現(xiàn)在我們只是把register函數(shù)的返回值保存在CounterStore對(duì)象的dispatcherToken字段上最爬,待會(huì)就會(huì)用得到尊剔。

現(xiàn)在我們仔細(xì)看看register接受的這個(gè)回調(diào)函數(shù)參數(shù),這是Flux流程中最核心的部分撰茎,當(dāng)通過register函數(shù)把一個(gè)回調(diào)函數(shù)注冊(cè)到Dispatcher之后愤估,所有派發(fā)給Dispatcher的action對(duì)象帮辟,都會(huì)傳遞到這個(gè)回調(diào)函數(shù)中來。

比如通過Dispatcher派發(fā)一個(gè)動(dòng)作玩焰,代碼如下:

AppDispatcher.dispatcher({

type:ActionTypes.INCREMENT,

counterCaption:'First'

});

那么CounterStore注冊(cè)的回調(diào)函數(shù)就會(huì)被調(diào)用由驹,唯一的一個(gè)參數(shù)就是那個(gè)action對(duì)象,回調(diào)函數(shù)要做的 昔园,就是根據(jù)action對(duì)象來決定該如何更新自己的狀態(tài)蔓榄。

作為一個(gè)普遍接受的傳統(tǒng),action對(duì)象中必有一個(gè)type字段默刚,類型是字符串甥郑,用于表示這個(gè)action對(duì)象是什么類型。

在我們的例子中羡棵,action對(duì)象的type和counter Caption字段結(jié)合在一起壹若,可以確定是哪個(gè)計(jì)數(shù)器應(yīng)該做加一或減一的動(dòng)作,上面例子中的動(dòng)作含義就是:“名字為First的 計(jì)數(shù)器要做加一動(dòng)作”。

無論是加一或者減一店展,最后都要調(diào)用CounterStore.emitChange函數(shù)养篓,假如有調(diào)用者通過Counter.addChangeListner關(guān)注了CounterStore的狀態(tài)變化,這個(gè)emitChange函數(shù)調(diào)用就會(huì) 引發(fā)監(jiān)聽函數(shù)的執(zhí)行赂蕴。

接下來柳弄,我們?cè)賮砜纯戳硪粋€(gè)Store,也就是代表所有計(jì)數(shù)器計(jì)數(shù)值總和的Store概说,在src/stores/SummaryStore.js中編寫源代碼碧注。

SummaryStore也有emitChange、addChangeListener還有removeChangeListener函數(shù)糖赔,功能一樣也是用于通知監(jiān)聽者狀態(tài)變化萍丐,這幾個(gè)函數(shù)的代碼和CounterStore中 完全重復(fù),不同點(diǎn)是對(duì)獲取狀態(tài)函數(shù)的定義放典,代碼如下:

? function? computeSummary(counterValues){

let summary = 0;

for(const key in counterValues){

if(counterValues hasOwnPoperty(key)){

summary+=counterVlues(key);

}

}

return summary;

}

const SummaryStore = Object.assign({},EventEmitter.prototype,{

getSummary:function(){

return computeSummary(CounterStore.getCounterValues());

}

});

可以注意到逝变,SummaryStore并沒有存儲(chǔ)自己的狀態(tài),當(dāng)getSummary被調(diào)用時(shí)奋构,它是直接從CounterStore里獲取狀態(tài) 計(jì)算的壳影。

CounterStore提供了getCounterValues函數(shù)讓其他模塊能夠獲得所有計(jì)數(shù)器的值,SummaryStore也提供了getSummary讓其他模塊可以獲得所有計(jì)數(shù)器當(dāng)前值的總和弥臼。不過宴咧,既然總可以通過CounterStore.getCounterValues函數(shù)獲取最新鮮的數(shù)據(jù),SummaryStore似乎也就沒有必要把計(jì)數(shù)器當(dāng)前值總和存儲(chǔ)到某個(gè)變量里径缅。事實(shí)上掺栅,可以看到SummaryStore并不像CounterStore一樣用一個(gè)變量counterValus存儲(chǔ)數(shù)據(jù),SummaryStorer不存儲(chǔ)數(shù)據(jù)芥驳,而是每次對(duì)getSummary的調(diào)用柿冲,都實(shí)時(shí)讀取CounterStore.getCounterValus,然后實(shí)時(shí)計(jì)算出總和返回給調(diào)用者兆旬。

可見假抄,雖然名為Store,但并不表示一個(gè)Store必須要存儲(chǔ)什么東西丽猬,Store只是提供獲取數(shù)據(jù)的方法宿饱,而Store提供的數(shù)據(jù)完全可以用另一個(gè)Store計(jì)算得來。脚祟。

SummaryStore在Dispatcher上注冊(cè)的回調(diào)函數(shù)

也和CounterStore很不一樣谬以,代碼如下:

SummaryStore.dispatcherToken = Dispatcher.register((action)=>{

? ? if((action.type===ActionTypes.INCREMENT)||

(action.type===ActionTypes.DECREMENT)){

AppDispatcher.waitFor([CounterStore.dispatchehToken]);

SummaryStore.emitChange();

? }

});

SummaryStore同樣也通過AppDispatcher.register函數(shù)注冊(cè)一個(gè)回調(diào)函數(shù)由桌,用于接受派發(fā)的action對(duì)象为黎。在回調(diào)函數(shù)中邮丰,也只關(guān)注INCREMENT和DECREMENT類型的action對(duì)象,并通過emitChange通知監(jiān)聽者铭乾,注意在這里使用了waitFor函數(shù)剪廉,這個(gè)函數(shù)解決的是下面描述的問題。

既然一個(gè)action對(duì)象會(huì)被派發(fā)給所有回調(diào)函數(shù)炕檩,這就產(chǎn)生了一個(gè)問題斗蒋,到底是按照什么順序調(diào)用各個(gè)回調(diào)函數(shù)呢?

即使Flux按照register調(diào)用的順序去調(diào)用各個(gè)回調(diào)函數(shù)笛质,我們也完全無法把握各個(gè)Store哪個(gè)先裝載從而調(diào)用register函數(shù)泉沾。所以,可以認(rèn)為Dispatcher調(diào)用回調(diào)函數(shù)的順序是無法預(yù)期的妇押,不要假設(shè)它會(huì)按照我們期望的順序逐個(gè)調(diào)用跷究。

設(shè)想一下,當(dāng)一個(gè)INCREMENT類型的動(dòng)作被派發(fā)了敲霍,如果首先調(diào)用SummaryStore的回調(diào)函數(shù)揭朝,在這個(gè)回調(diào)函數(shù)中立即用emitChange通知了監(jiān)聽者,這時(shí)監(jiān)聽者會(huì)立即通過SummarryStore的getSummary獲取結(jié)果色冀,而這個(gè)getSummary是通過CounterStore暴露的getCounterValues函數(shù)獲取當(dāng)前計(jì)數(shù)器值,計(jì)算總和返回......然而柱嫌,這時(shí)候锋恬,INCREMENT動(dòng)作還沒來得及派發(fā)到CounterStore啊编丘!也就是說与学,CounterStore的getCounterrValues返回的還是一個(gè)未更新的值,那樣SummaryStore的getSummary返回值也就是一個(gè)錯(cuò)誤值了嘉抓。

于是索守,解決這個(gè)問題就靠Dispatcher的waitFor函數(shù)。在SummaryStore的回調(diào)函數(shù)中抑片,之前在CounterStore中注冊(cè)回調(diào)函數(shù)時(shí)保存下來的dispatcherToken終于派上用場(chǎng)了卵佛。

Dispatcher的waitFor可以接收一個(gè)數(shù)組作為參數(shù),數(shù)組中的每個(gè)元素都是一個(gè)Disppatcher.register函數(shù)的返回結(jié)果敞斋,也就是所謂的dispatcherToken截汪。這個(gè)waitFor函數(shù)告Dispatcher,當(dāng)前的處理必須要暫停植捎,直到dispatcherToken代表的那些已注冊(cè)回調(diào)函數(shù)執(zhí)行結(jié)束才能繼續(xù)衙解。

我們知道,JavaScript是單線程的語音焰枢,不可能有線程等待這回事蚓峦,這個(gè)waitFor函數(shù)當(dāng)然并不是多線程實(shí)現(xiàn)的舌剂,只是在調(diào)用waitFor的時(shí)候,把控制權(quán)交給Dispatcher暑椰,讓Dispatcher檢查一下dispatcherToken代表的回調(diào)函數(shù)有沒有被執(zhí)行霍转,如果已經(jīng)執(zhí)行,那就直接繼續(xù)干茉,如果還沒有執(zhí)行谴忧,那就調(diào)用dispatcherToken代表的回調(diào)函數(shù)之后waitFor才返回。

回到上面假設(shè)的例子角虫,即使SummaryStore比CounterStore提前接收到了action對(duì)象沾谓,在emitChange中調(diào)用waitFor,也就能夠保證在emitChange函數(shù)被調(diào)用的時(shí)候戳鹅,CounterStore也已經(jīng)處理過這個(gè)action對(duì)象均驶,一切完美解決。

這里要注意一個(gè)事實(shí)枫虏,Dispatcher的register函數(shù)妇穴,只提供了注冊(cè)一個(gè)回調(diào)函數(shù)的功能,但卻不能讓調(diào)用者在register時(shí)選擇只監(jiān)聽某些action隶债,換句話說腾它,每個(gè)register的調(diào)用者只能這樣請(qǐng)求:“當(dāng)有任何動(dòng)作被派發(fā)時(shí),請(qǐng)調(diào)用我死讹÷鞯危”但不能夠這么請(qǐng)求:“當(dāng)這種類型或者那種類型被派發(fā)的時(shí)候,請(qǐng)調(diào)用我”赞警。

當(dāng)一個(gè)動(dòng)作被派發(fā)的時(shí)候妓忍,Dispatcher就是簡(jiǎn)單地把所有注冊(cè)的回調(diào)函數(shù)全部都調(diào)用一遍,至于這個(gè)動(dòng)作是不是對(duì)方關(guān)心的愧旦,F(xiàn)lux的Dispatcher不關(guān)心世剖,要求每個(gè)回調(diào)函數(shù)去鑒別。

看起來笤虫,這似乎是一種浪費(fèi)旁瘫,但是這個(gè)設(shè)計(jì)讓Flux的Dispatcher邏輯最簡(jiǎn)單話,Dispatcher的責(zé)任越簡(jiǎn)單琼蚯,就越不會(huì)出現(xiàn)問題境蜕。畢竟,由回調(diào)函數(shù)全權(quán)決定如何處理actioon對(duì)象凌停,也是非常合理的粱年。

4. view

首先需要說明,F(xiàn)lux框架下罚拟,View并不是說必須使用React台诗,View本身是一個(gè)獨(dú)立的部分完箩,可以用任何一種UI 庫(kù)來實(shí)現(xiàn)。

不過拉队,話說回來弊知,既然我們都使用上Flux了,除非項(xiàng)目有大量歷史遺留代碼需要利用粱快,否則實(shí)在沒有理由不用React來實(shí)現(xiàn)view秩彤。

存在于Flux框架中的React組件需要實(shí)現(xiàn)以下幾個(gè)功能:

①創(chuàng)建時(shí)要讀取Store上狀態(tài)來初始化組件內(nèi)部狀態(tài)。

②當(dāng)Store上狀態(tài)發(fā)生變化時(shí)事哭,組件要立刻同步更新組件內(nèi)部狀態(tài)保持一致漫雷;

③View如果要改變Store狀態(tài),必須而且只能派發(fā)action鳍咱。

最后讓我們來看看例子中的View部分降盹,為了方便管理,所有的View文件都放在src/views目錄下谤辜。

先看src/vies/ControlPanel.js中的ControlPanel組件蓄坏,其中render函數(shù)的實(shí)現(xiàn)和上一章很不一樣,代碼如下:

render(){

return(

<div style={ style }>

<Counter caption ="First"/>

<Counter caption ="Seond"/>

<Counter caption ="Third"/>

<hrr/>

<Summary />

</div>

);

}

可以注意到丑念,和前面章節(jié)中的ControlPanel不同涡戳,Counter組件實(shí)例只有caption屬性,沒有initValues屬性脯倚。因?yàn)槲覀儼延?jì)數(shù)值包括初始值全都放到CounterStore中去了妹蔽,所以在創(chuàng)造Counter組件實(shí)例的時(shí)候就沒有必要指定initValue了。

接著看src/views/Counter組件挠将,構(gòu)造函數(shù)中初始化this.state的方式有了變化,代碼如下:

constructor(props){

super(props);

this.onChange = this.onChange.bind(this);

this.onClickIncrementButton =this.onClickIncrementButton.bind(this);

this.onClickDecrementButton= this.onClickDecrementButton.bind(this);

this.state = {

count:CounterStore.getCounterValues()[props.caption]

}

}

在構(gòu)造函數(shù)中编整,CounterStore.getCounterValues函數(shù)獲得了所有計(jì)數(shù)器的當(dāng)前值舔稀,然后把this.state初始化為對(duì)應(yīng)caption字段的值,也就是說Counter組件store來源不再是prop掌测,而是Flux的Store内贮。

Counter組件中的state應(yīng)該成為Flux Store上狀態(tài)的一個(gè)同步鏡像,為了保持兩者一致汞斧,除了在構(gòu)造函數(shù)中的初始化之外夜郁,在之后CounterStore上狀態(tài)變化時(shí),Counter組件也要對(duì)應(yīng)變化粘勒,相關(guān)代碼如下:

componentDidMount(){

CounterStore.addChangeListener(this.onChange);

}

componentWillUnmount(){

CounterStore.removeChangeListener(this.onCange);

}

onChange(){

const newCount = CounterStore.getCounterValues()[this.props.caption];

this.setState({count:newCount});

}

如上面的代碼所示竞端,在componentDidMount函數(shù)中通過CounterStore.addChangeListener函數(shù)監(jiān)聽了CounterStore的變化之后,只有CounterStore發(fā)生化庙睡,Couunter組件的onChange函數(shù)就會(huì)被調(diào)用事富。與componentDidMount函數(shù)中監(jiān)聽事件相對(duì)應(yīng)技俐,在componentWillUnmount函數(shù)中刪除了這個(gè)監(jiān)聽。

接下來统台,要看React組件如何派發(fā)action雕擂,代碼如下:

onClickIncrementButton(){

Action.increment(this.props.caption);

}

onClickDecrementButton(){

Actions.decrement(this.props.caption);

}

render(){

const { caption } =this.props;

return(

<div>

<button style={buttonStyle} onClick={this.onClickIncrementButton} >+</button>

<button style={buttonStyle} onClick={this.onClickDecrementButton} >-</button>

<span>{caption} count:{this.state.count}</span>

</div>

);

}

可以注意到,在Counter組件中有兩處用到CounterStore的getCounterValues函數(shù)的地方贱勃,第一處是在構(gòu)造函數(shù)中初始化this.state的時(shí)候井赌,第二處是在響應(yīng)CounterStorre狀態(tài)變化的onChange函數(shù)中,同樣一個(gè)Store狀態(tài)贵扰,為了轉(zhuǎn)換為React組件的狀態(tài)仇穗,有兩次重復(fù)的調(diào)用,這看起來似乎不是很友好拔鹰。但是仪缸,React組件的狀態(tài)就是這樣,在構(gòu)造函數(shù)中要對(duì)this.state初始化列肢,要更新它就要調(diào)用this.setState函數(shù)恰画。

有沒有更簡(jiǎn)潔的辦法?比如說只使用CounterStore.getCounterValues一次瓷马?可惜拴还,只要我們想用組件的狀態(tài)來驅(qū)動(dòng)組件的渲染,就不可避免要有這兩步欧聘。那么如果我們不利于用組件的狀態(tài)呢片林?

如果不使用組件的狀態(tài),那么我們就可以逃出這個(gè)必須在代碼中使用Store兩次的宿命怀骤,在接下來的章節(jié)里费封,我們會(huì)遇到這種“無狀態(tài)”組件。

Summary組件蒋伦,存在于src/views/Summary.js弓摘,和Counter類似,在constructor中初始化組件狀態(tài)痕届,通過在componentDidMount中添加SummaryStore的監(jiān)聽來同步狀態(tài)韧献,因?yàn)檫@個(gè)View不會(huì)有任何交互功能,所以沒有派發(fā)出任何action研叫。

3.1.3Flux的優(yōu)勢(shì)

本章例子和第二章只用React的實(shí)現(xiàn)效果一樣锤窑,但是工作方式有了大變化。

回顧一下完全只使用React實(shí)現(xiàn)的版本嚷炉,應(yīng)用的狀態(tài)數(shù)據(jù)只存在于React組件之中渊啰,每個(gè)組件都要維護(hù)驅(qū)動(dòng)自己渲染的狀態(tài)數(shù)據(jù),單個(gè)組件的狀態(tài)還好維護(hù)申屹,但是如果多個(gè)組件之間的狀態(tài)有關(guān)聯(lián)虽抄,那就麻煩了走搁。比如Counter組件和Summary組件,Summary組件要維護(hù)所有Counter組件計(jì)數(shù)值的總和迈窟,Counter組件和Summary分別維護(hù)自己的狀態(tài)私植,如何同步Summary和Counter狀態(tài)就成了問題,React只提供了props方法讓組件之間通信车酣,組件之間關(guān)系稍微復(fù)雜一點(diǎn)曲稼,這種方式就顯得非常笨拙。湖员。

Flux的架構(gòu)下贫悄,應(yīng)用的狀態(tài)放在了Store中,React組件只是扮演View的作用娘摔,被動(dòng)根據(jù)Store的狀態(tài)來渲染窄坦。在上面的例子中,React組件依然有自己的狀態(tài)凳寺,但是已經(jīng)完全淪為Store組件的一個(gè)映射鸭津,而不是主動(dòng)變化的數(shù)據(jù)。

在完全只用React實(shí)現(xiàn)的版本里肠缨,用戶的交互操作逆趋,比如點(diǎn)擊“+“按鈕,引發(fā)的時(shí)間處理函數(shù)直接通過this.setState改變組件的狀態(tài)晒奕。在Flux的實(shí)現(xiàn)版本里闻书,用戶的操作引發(fā)的是一個(gè)“動(dòng)作“的派發(fā),這個(gè)派發(fā)的動(dòng)作會(huì)發(fā)送給所有的Store對(duì)象脑慧,引起Store對(duì)象的狀態(tài)改變魄眉,而不是直接引發(fā)組件的狀態(tài)改變。因?yàn)榻M件的狀態(tài)是Store狀態(tài)的映射闷袒,所以改變了Store對(duì)象也就觸發(fā)了React組件對(duì)象的狀態(tài)改變坑律,從而引發(fā)了界面重新渲染。

Flux帶來了什么好處呢霜运?最重要的就是“單向數(shù)據(jù)流”的管理方式。

在Flux的理念里蒋腮,如果要改變界面淘捡,必須改變Store中的狀態(tài),如果要改變Store中的狀態(tài)池摧,必須派發(fā)一個(gè)action對(duì)象焦除,這就是規(guī)矩。在這個(gè)規(guī)矩之下作彤,想要追溯一個(gè)應(yīng)用的邏輯就變得非常容易膘魄。

我們已經(jīng)討論過MVC框架的缺點(diǎn)乌逐,MVC最大的問題就是無法禁絕View和Model直接的直接對(duì)話,對(duì)應(yīng)于MVC中View就是Flux中的View创葡,對(duì)應(yīng)于MVC中的Model就是Flux中的Store浙踢,在Flux中,Store只有g(shù)et方法灿渴,沒有set方法洛波,根本不可能直接去修改其內(nèi)部狀態(tài),View只能通過get方法獲取Store的狀態(tài)骚露,無法直接去修改狀態(tài)蹬挤,如果View想要修改Store狀態(tài)的話,只有派發(fā)一個(gè)action對(duì)象給Dispatcher棘幸。

這看起來是一個(gè)“限制”焰扳,但卻是一個(gè)很好的“限制”,禁絕了數(shù)據(jù)流混亂的可能误续。

簡(jiǎn)單來說吨悍,在Flux的體系下,驅(qū)動(dòng)界面改變始于一個(gè)動(dòng)作的派發(fā)女嘲,別無他法畜份。

3.1.4Flux的不足

任何工具不可能只有優(yōu)點(diǎn)沒有缺點(diǎn),接下來讓我們看看Flux的不足之處欣尼,只有了解了Flux的不足之處爆雹,才能理解為什么會(huì)出現(xiàn)Flux的改進(jìn)框架Redux。

1.Store之間依賴關(guān)系

在Flux的體系中愕鼓,如果兩個(gè)Store之間有邏輯依賴關(guān)系钙态,就必須用上Dispatcher的waitFor函數(shù)。在上面的例子中我們已經(jīng)使用過waitFor函數(shù)菇晃,SummmarryStore對(duì)action類型的處理册倒,依賴于CounterStore已經(jīng)處理過了。所以磺送,必須要通過waitFor函數(shù)告訴Dispatcher驻子,先讓CounterStorer處理這些action對(duì)象,只有CounterStore搞定之后SummaryStore才繼續(xù)估灿。

那么崇呵,SummaryStore如何標(biāo)識(shí)CounterStore呢?靠的是register函數(shù)的返回值dispatchToken馅袁。

而dispatcherToken的產(chǎn)生域慷,當(dāng)然是CounterStore控制的,換句話說,要這樣設(shè)計(jì):

□ CounterStore必須要把注冊(cè)回調(diào)函數(shù)產(chǎn)生的dispatchToken公布與眾犹褒。

□ SummaryStore必須要在代碼里建立對(duì)CounterStore的dispatchToken的依賴抵窒。

雖然Flux這個(gè)設(shè)計(jì)的確解決了Store之間的依賴關(guān)系,但是叠骑,這樣明顯的模塊之間的依賴李皇,看著還是讓人感覺不太舒服,畢竟座云,最后的依賴管理是根本不讓依賴產(chǎn)生的疙赠。

2.難以進(jìn)行服務(wù)器端渲染

關(guān)于服務(wù)器端渲染,我們?cè)诤竺娴恼鹿?jié)(第12章)“同構(gòu)”中詳細(xì)介紹朦拖,在這里圃阳,我們只需要知道,如果要在服務(wù)器端渲染璧帝,輸出不是一個(gè)DOM樹捍岳,而是一個(gè)字符串,準(zhǔn)確來說就是一個(gè)全是HTML的字符串睬隶。

在Flux的體系中锣夹,有一個(gè)全局的Dispatch,然后每一個(gè)Store都是一個(gè)全局唯一的對(duì)象苏潜,這對(duì)于瀏覽器端應(yīng)用完全沒有問題银萍,但是如果放在服務(wù)器端,就會(huì)有大問題恤左。

和一個(gè)瀏覽器只服務(wù)于一個(gè)用戶不同贴唇,在服務(wù)器端要同時(shí)接受很多用戶的請(qǐng)求,如果每個(gè)Store都是全局唯一的對(duì)象飞袋,那不同請(qǐng)求的狀態(tài)肯定就亂套了戳气。

并不是說Flux不能做服務(wù)器端的渲染,只是說Flux做服務(wù)器端渲染很困難巧鸭,實(shí)際上瓶您,F(xiàn)acebook也說的很清楚秦陋,F(xiàn)lux不是設(shè)計(jì)用作服務(wù)器端渲染的携兵,他們也從來沒有嘗試過把Flux應(yīng)用于服務(wù)器端击罪。

3.Store混雜了邏輯和狀態(tài)

Store封裝了數(shù)據(jù)和處理數(shù)據(jù)的邏輯芙扎,用面向?qū)ο笏季S來看,這是一件好事猪勇,畢竟對(duì)象就是這樣定義的宦赠。但是骤坐,當(dāng)我們需要?jiǎng)討B(tài)替換一個(gè)Store的邏輯時(shí)锻拘,只能把這個(gè)Store整體替換掉油吭,那也就無法保持Store中存儲(chǔ)的狀態(tài)。

讀者可能會(huì)問署拟,有什么場(chǎng)景是需要替換Store的呢婉宰?

在開發(fā)模式下,開發(fā)人員要不停的對(duì)代碼進(jìn)行修改推穷,如果Store在某個(gè)狀態(tài)下引發(fā)了bug心包,如果能在不毀掉狀態(tài)的情況下替換Store的邏輯,那就最好了馒铃,開發(fā)人員就可以不斷地改進(jìn)邏輯來驗(yàn)證這個(gè)狀態(tài)下bug是否修復(fù)了蟹腾。

還有一些應(yīng)用,在生產(chǎn)環(huán)境下就要根據(jù)用戶屬性來動(dòng)態(tài)加載不同的模塊区宇,而且動(dòng)態(tài)加載模塊還希望不要網(wǎng)頁(yè)重新加載娃殖,這時(shí)候也希望能夠在不修改應(yīng)用狀態(tài)的前提下重新加載應(yīng)用邏輯,這就是熱加載(Hot Load)议谷,在第12章會(huì)詳細(xì)介紹如何實(shí)現(xiàn)熱加載炉爆。

可能讀者會(huì)覺得這里所說的“偷梁換柱”一樣的替換應(yīng)用邏輯是不能做到的。實(shí)際上卧晓,真的能夠做到芬首,Redux就能做到,所以讓我們進(jìn)入Redux的世界吧逼裆。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末郁稍,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子胜宇,更是在濱河造成了極大的恐慌耀怜,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,000評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掸屡,死亡現(xiàn)場(chǎng)離奇詭異封寞,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)仅财,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門狈究,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人盏求,你說我怎么就攤上這事抖锥。” “怎么了碎罚?”我有些...
    開封第一講書人閱讀 168,561評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵磅废,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我荆烈,道長(zhǎng)拯勉,這世上最難降的妖魔是什么竟趾? 我笑而不...
    開封第一講書人閱讀 59,782評(píng)論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮宫峦,結(jié)果婚禮上岔帽,老公的妹妹穿的比我還像新娘。我一直安慰自己导绷,他們只是感情好犀勒,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,798評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著妥曲,像睡著了一般贾费。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上檐盟,一...
    開封第一講書人閱讀 52,394評(píng)論 1 310
  • 那天褂萧,我揣著相機(jī)與錄音,去河邊找鬼葵萎。 笑死箱玷,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的陌宿。 我是一名探鬼主播锡足,決...
    沈念sama閱讀 40,952評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼壳坪!你這毒婦竟也來了舶得?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,852評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤爽蝴,失蹤者是張志新(化名)和其女友劉穎沐批,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蝎亚,經(jīng)...
    沈念sama閱讀 46,409評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡九孩,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,483評(píng)論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了发框。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片躺彬。...
    茶點(diǎn)故事閱讀 40,615評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖梅惯,靈堂內(nèi)的尸體忽然破棺而出宪拥,到底是詐尸還是另有隱情,我是刑警寧澤铣减,帶...
    沈念sama閱讀 36,303評(píng)論 5 350
  • 正文 年R本政府宣布她君,位于F島的核電站,受9級(jí)特大地震影響葫哗,放射性物質(zhì)發(fā)生泄漏缔刹。R本人自食惡果不足惜球涛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,979評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望校镐。 院中可真熱鬧宾符,春花似錦、人聲如沸灭翔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肝箱。三九已至,卻和暖如春稀蟋,著一層夾襖步出監(jiān)牢的瞬間煌张,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工退客, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留骏融,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,041評(píng)論 3 377
  • 正文 我出身青樓萌狂,卻偏偏與公主長(zhǎng)得像档玻,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子茫藏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,630評(píng)論 2 359

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