Redux對(duì)于React程序是可有可無(wú)的嗎?當(dāng)你認(rèn)識(shí)到Redux在編程時(shí)給你那種可以掌控一切狀態(tài)能力的時(shí)候,你會(huì)覺(jué)得如果沒(méi)有這種思考方法,我們到底該怎么來(lái)實(shí)現(xiàn)其中的邏輯呢?React中對(duì)于組件的控制引入了兩個(gè)東西一個(gè)是props,一個(gè)是state.如果站在單個(gè)組件的角度上,組件本身其實(shí)也是一個(gè)復(fù)雜的復(fù)合體,給我們只開(kāi)放兩個(gè)能決定組件可以做什么,可以怎么表現(xiàn)的接口,就是這兩個(gè)東西. 哲學(xué)說(shuō)(呵呵,不是我說(shuō)的,后面文章里的東西):一切事物皆由外因.外因的來(lái)源其實(shí)也有很多種組織方法.我曾經(jīng)踢過(guò)很長(zhǎng)時(shí)間的足球,只是踢踢野球罷了,踢野球可是沒(méi)有任何章法可言,對(duì)陣雙方可能是雜合著各種踢法和戰(zhàn)術(shù),或者是沒(méi)有戰(zhàn)術(shù),好像也是在踢足球,但是大家都是憑著愛(ài)好和從觀看比賽時(shí)積累的一點(diǎn)不成熟的想法.但是和有專門戰(zhàn)術(shù)訓(xùn)練,有固定的戰(zhàn)術(shù)打法的職業(yè)隊(duì)一比沒(méi)有任何的戰(zhàn)斗能力.即便大家看不上的中國(guó)足球,隨便拿一只大學(xué)校隊(duì)級(jí)別的球隊(duì)和野球隊(duì)比賽,野球隊(duì)都差的十萬(wàn)八千里.這里不談個(gè)人的技術(shù)素養(yǎng).在場(chǎng)地上的球員都是按照既定的方案在運(yùn)行,也即是其實(shí)受到教練和戰(zhàn)術(shù)訓(xùn)練的控制的.這一點(diǎn)非常的高效啊.我都不知道我怎么把React/Redux的思想和足球的思想聯(lián)系了起來(lái).但是好像能說(shuō)明一點(diǎn)問(wèn)題. 還有就是要使用新思想,腦袋要給新思想完全的空間. 中國(guó)足球隊(duì)現(xiàn)在請(qǐng)了世界頂級(jí)的教練,成績(jī)依然不是太好,既然請(qǐng)了人家的教練,就完全的配合,不要用什么中國(guó)國(guó)情特殊來(lái)對(duì)抗教練的思想.這對(duì)于Redux在React中的實(shí)施也是很重要的.要用Redux就要完全接受這套思想,不要老想著這怎么和原來(lái)的不一樣啊!不能這么搞! 重要的還是思想. Facebook在實(shí)現(xiàn)React已經(jīng)給state的管理提供了一個(gè)很好的選型構(gòu)架,Redux只不過(guò)這比較好的實(shí)現(xiàn)了這個(gè)構(gòu)架.別猶豫了,要使用React,徹底的投入Redux的懷抱吧!**再啰嗦總結(jié)一下:Redux通過(guò)props完全掌控了React組件的一舉一動(dòng),我們可以在組件外觀察組件能做什么,會(huì)發(fā)生什么變化 **
下面的這篇文章在meidum中收到了1000過(guò)個(gè)贊,的確是值得推薦.其實(shí)根本的問(wèn)題Redux文檔中的三個(gè)原則已經(jīng)總結(jié)的太好了.但是思想的轉(zhuǎn)變真的不是一朝一夕的功夫!需要花更多的時(shí)間給你的大腦回路形成的時(shí)間.一旦你的大腦再看到Redux的時(shí)候,有一個(gè)神經(jīng)元激活了,那就基本成功了(??,題外話,喜歡美劇老友記的可以搜搜鏡像神經(jīng)元和珍妮弗.安妮斯頓.非常有意思的研究).
以下為譯文內(nèi)容
當(dāng)我開(kāi)始使用React的時(shí)候,Redux還沒(méi)出生呢?僅僅只有Flux構(gòu)架以及一堆實(shí)現(xiàn)這個(gè)構(gòu)架的方案.
現(xiàn)在在React的數(shù)據(jù)管理方面有兩個(gè)明顯的勝利者:Redux和MobX,MobX甚至都沒(méi)有使用Flux架構(gòu).Redux之所以這么吸引眼球,原因他不在僅僅是為React服務(wù)了.你可以發(fā)現(xiàn)Redux在其他的框架上也已經(jīng)有實(shí)施方案.包括Angular2,例如ngrx:store.
note1 MobX非???在簡(jiǎn)單的UI中我可能會(huì)使用它而不是Redux,MobX更簡(jiǎn)單,也不太啰嗦(譯注:的確是,你想要既簡(jiǎn)單又高效的東西那不可能.這種事情是不會(huì)發(fā)生的).這也就是說(shuō)Redux有一些特性是MobX不能給你的.在項(xiàng)目中是否使用Redux之前理解這些Redux的獨(dú)有特性是非常重要的.
note2 Relay和falcor是另外兩個(gè)狀態(tài)管理的解決方案.但是他們分別要由GraphQL和Falcor server來(lái)提供后臺(tái)支持.Relay的state都和服務(wù)器端的持久化數(shù)據(jù)對(duì)應(yīng)著.AFAIK(目前我知道的是),兩者都不能提供客戶端暫時(shí)的
state管理方案.你可能會(huì)很喜歡結(jié)合Relay或者Falcor和Redu或MobX,結(jié)合使用其中在服務(wù)端和客戶端state管理的能力.底線:在客戶端的state管理上沒(méi)有明顯的勝利者.使用手頭可以用的最好工具.
Redux的創(chuàng)建者Dan Abramov有一系列關(guān)于這個(gè)主題的課程:
兩者都是循序漸進(jìn)的介紹Redux的基礎(chǔ).但是你會(huì)需要對(duì)Redux的理解提升到更高的水平.
下面的這些小Tips幫助你構(gòu)建更好的Redux app
1. Understanding the Benefits of Redux
Redux有兩個(gè)至關(guān)重要的目標(biāo),你要牢記于心:
- View 渲染的確定性(Deterministic)
- State變化的確定性(Deterministic)
確定性對(duì)于應(yīng)用的測(cè)試,診斷和修復(fù)bugs來(lái)說(shuō)都是非常重要的.如果你的應(yīng)用視圖和狀態(tài)是不確定的(nondeterministic)的,根本就不可能知道視圖和狀態(tài)是不是有效. 或許nonndeterministic本身就是一個(gè)bug.
但是有些事情內(nèi)在就是nodedeterministic的,例如用戶輸入和網(wǎng)絡(luò)的I/O操作.我們?cè)趺粗来a是否正常工作呢? 簡(jiǎn)單:隔離.(譯注:為什么說(shuō)好測(cè)試呢?舉個(gè)例子:
你要是血糖高,流動(dòng)在血管里的血是沒(méi)有辦法測(cè)糖含量的,我們把它抽出來(lái),才能用試紙來(lái)測(cè)量.這就是所謂的隔離或者分離啊)
Redux的主要目的就是要從I/O端的異步操作例如視圖的渲染和網(wǎng)絡(luò)的工作中把state management隔離出來(lái).當(dāng)異步操作隔離出來(lái)以后,代碼就變得非常的簡(jiǎn)單,立即和測(cè)試業(yè)務(wù)代碼也非常的簡(jiǎn)單了,因?yàn)檫@些代碼不再和網(wǎng)絡(luò)請(qǐng)求以及DOM操作糾纏在一起.
當(dāng)你的視圖渲染從網(wǎng)絡(luò)I/O和狀態(tài)更新隔離開(kāi)來(lái)以后,你就會(huì)得到一個(gè)外因決定的 視圖渲染方法,意思是:只要給定相同的state,視圖一定會(huì)渲染出相同的結(jié)果.這么做消除了諸如異步操作中競(jìng)爭(zhēng)條件對(duì)于視圖的影響以及視圖在渲染過(guò)程中對(duì)state的不完整的截取問(wèn)題.
當(dāng)一個(gè)新手在思考創(chuàng)建一個(gè)視圖的時(shí)候,他可能會(huì)想:這里需要一個(gè)用戶模型,所以我啟動(dòng)一個(gè)異步請(qǐng)求,當(dāng)promises對(duì)象的狀態(tài)變?yōu)閞esloves的時(shí)候,我就使用用戶的名字來(lái)更新用戶組件.這么做比todo中的 items需要的任務(wù)多一寫(xiě),我們使用fetch模塊來(lái)完成他,當(dāng)promise對(duì)象resolves的時(shí)候,我們可以遍歷他們,添加到屏幕上.
使用這種方法有幾個(gè)主要的問(wèn)題:
- 在任何時(shí)間點(diǎn),你都沒(méi)有所有需要渲染視圖的數(shù)據(jù).直到組件開(kāi)始加載之前,你實(shí)際都不能開(kāi)始請(qǐng)求數(shù)據(jù).
- 不同的遠(yuǎn)程請(qǐng)求任務(wù)會(huì)在不同時(shí)間點(diǎn)到來(lái),這會(huì)對(duì)視圖渲染的隊(duì)列有些微妙的變化.為了明白渲染隊(duì)列,你需要一些你不可能預(yù)料的知識(shí):每一個(gè)異步請(qǐng)求的持續(xù)時(shí)間.小測(cè)試:在上面的場(chǎng)景里,那個(gè)視圖最先渲染?用戶組件還是todo-items?答案是:這是競(jìng)爭(zhēng)關(guān)系.
- 有時(shí)候事件監(jiān)聽(tīng)器也會(huì)更新視圖的狀態(tài),可能還會(huì)觸發(fā)另一個(gè)渲染,更復(fù)雜的隊(duì)列.
關(guān)鍵的問(wèn)題是:在視圖的狀態(tài)里存儲(chǔ)數(shù)據(jù),在視圖中添加事件監(jiān)聽(tīng)器導(dǎo)致了視圖狀態(tài)的突變:
Nondeterminism=并發(fā)的處理過(guò)程+共享的狀態(tài).-Martin Odersky(Scalas設(shè)計(jì)者)
數(shù)據(jù)遠(yuǎn)程獲取,數(shù)據(jù)操作,視圖渲染混合在一起構(gòu)成了時(shí)間旅行式的意大利面條代碼
我知道這里所說(shuō)的好像是B級(jí)科幻電影的套路,但是請(qǐng)相信我,時(shí)間旅行式的意大利面是最壞的菜譜!
flux構(gòu)架強(qiáng)調(diào)嚴(yán)格的隔離和序列,每一次處理都會(huì)遵守這些規(guī)則.
- 首先,我們會(huì)知道,固定的state...
- 當(dāng)我們?cè)阡秩疽晥D的時(shí)候,沒(méi)有任何事情可以改變state.
- 給定同樣的state,渲染的視圖總是相同.
- 事件監(jiān)聽(tīng)器監(jiān)聽(tīng)用戶的輸入和網(wǎng)絡(luò)請(qǐng)求句柄.當(dāng)這些請(qǐng)求有了結(jié)果以后,actions會(huì)被發(fā)送到store.
- 當(dāng)一個(gè)action被派發(fā),state被更新到一個(gè)新的已知的state,隊(duì)列重復(fù).可以改變state的只有派發(fā)actions.
Flux構(gòu)架是一個(gè)果殼,單向的數(shù)據(jù)流動(dòng)構(gòu)架.

在Flux構(gòu)架中,視圖監(jiān)聽(tīng)用戶的輸入,把這些輸入翻譯為action對(duì)象,action對(duì)象可以被dispatch到Store.Store更新應(yīng)用的state,并且告知視圖再次渲染.當(dāng)然了,視圖也可以只依賴輸入和事件,這也沒(méi)有問(wèn)題.另外,事件監(jiān)聽(tīng)器可以像下面這樣派發(fā)action對(duì)象:
重要的是,Flux中的state更新是事務(wù)性的.代替在state上簡(jiǎn)單的調(diào)用更新方法,或者支架操作一個(gè)值,action會(huì)被派發(fā)到store.一個(gè)Action對(duì)象會(huì)被事務(wù)性的記錄下來(lái).你可以認(rèn)為這有點(diǎn)像銀行的事務(wù)操作-變化記錄的生成.
當(dāng)你在銀行存一點(diǎn)錢,你賬戶五分鐘前的信息不回被刪除.新的賬戶信息會(huì)被添加到交易的歷史記錄中. Action對(duì)象在你的應(yīng)用state中添加一個(gè)事務(wù)歷史記錄.
Action對(duì)象是這個(gè)樣子的:
{
type: ADD_TODO,
payload: 'Learn Redux'
}
action對(duì)象給你的能力是保持所有的state變化的日志資料.這個(gè)日志可以根據(jù)外部條件重復(fù)生成,意思是:
給定相同的初始化state和相同的事務(wù)處理,在相同的操作中,你可以獲得相同的state.
這一點(diǎn)的潛意義是:
- 容易測(cè)試
- undo/redo很容易實(shí)施
- 時(shí)間旅行deguging
- 持久性-即使state被擦寫(xiě)掉了,如果你有每個(gè)事務(wù)的處理記錄,可以很容易的重復(fù)他.
試問(wèn),誰(shuí)不想掌控時(shí)空的變化?事務(wù)性的狀態(tài)給你了時(shí)間旅行的超能力.

2. 某些Apps不需要Redux
如果你的UI的工作流很簡(jiǎn)單,Redux所做的就有點(diǎn)大材小用了.如果你在做一個(gè)tic-tac-toe的游戲,你真的需要undo/redo?這些小游戲很少能玩超過(guò)一分鐘.如果玩家失敗了,你需要做的只是重置游戲,再次開(kāi)始就行了.
如果:
- 用戶流程很簡(jiǎn)單
- 用戶之間沒(méi)有交互
- 你不需要管理服務(wù)端事件(SSE)或者websockets
- 每個(gè)視圖從單一數(shù)據(jù)源獲取數(shù)據(jù).
這可能是因?yàn)槟愕腶pp 事件流程太簡(jiǎn)單,不值得在state的事務(wù)上花費(fèi)額外的精力.
或許你不需要在app中使用Fluxify化.還有一個(gè)簡(jiǎn)單的解決方案.看看MobX (譯注:我很慶幸沒(méi)有在Redux學(xué)習(xí)遇到難點(diǎn)的時(shí)候,退縮到安全地帶,有的朋友遇到Redux學(xué)不下去的時(shí)候就退到了MobX了,在我看來(lái)Redux是職業(yè)足球隊(duì)的踢法,MobX是業(yè)余球隊(duì)踢法).
然而,隨著你的app變得復(fù)雜,視圖的復(fù)雜性和狀態(tài)管理性都增加了,事務(wù)性狀態(tài)價(jià)值增加,MobX不能提供狀態(tài)的事務(wù)性管理方法.
如果:
- 用戶的流程很復(fù)雜
- 你的app有很多的用戶工作流
- 用戶有很多交互聯(lián)系
- 正在使用web sockets或者SSE
- 從不同的數(shù)據(jù)來(lái)源獲取數(shù)據(jù)構(gòu)建單一的視圖(譯注:這個(gè)能力是React比MVC框架更高級(jí)的地方,頁(yè)面中視圖中的不同組件可以各自獲取自己的數(shù)據(jù),互相不受干擾)
如果能從事務(wù)模型中獲益.Redux對(duì)你就非常的合適.
那么關(guān)于web sockets和SSE? 當(dāng)你添加更多的異步I/O來(lái)源時(shí),理解app內(nèi)部的狀態(tài)管理變得非常的困難.受外部控制的state和state的事務(wù)性處理能簡(jiǎn)化理解過(guò)程(譯注:redux-logger打印出state的結(jié)構(gòu)的時(shí)候,我如釋重負(fù)).
我的觀點(diǎn)是:大多數(shù)的SaaS產(chǎn)品包含了最新的復(fù)雜UI工作流,應(yīng)該使用事務(wù)性state管理.小型的工具app或者簡(jiǎn)單原型的app不應(yīng)該用.用對(duì)工具是非常重要的.
3.理解Reducers
Redux=Flux+Functional Programming
Flux使用action對(duì)象描述了單向的數(shù)據(jù)流和事務(wù)狀管理,但是對(duì)于怎么操作action對(duì)象,什么也沒(méi)有講.這就是Redux的切入點(diǎn).
構(gòu)建Redux state管理的首要模塊就是reducer函數(shù).那么,什么是reducer 函數(shù)?
在函數(shù)式編程中,普通的工具reducer()或者fold()用來(lái)對(duì)values列表中的每一個(gè)value執(zhí)行reducer函數(shù),累加單個(gè)輸出的value.這里有一個(gè)對(duì)于JavaScript Array.prototype.reduce()原型的總結(jié).
//這是從codepen拿出來(lái)的代碼,瀏覽器console可以看到結(jié)果
const initialState=0;
const reducer=(state= initialState,data)=>state+data;
const total=[0,1,2,3].reduce(reducer);
console.log(total);
在Redux中使用reducers,不是對(duì)數(shù)組進(jìn)行操作,而是對(duì)系列的action對(duì)象應(yīng)用reducer.記住,action對(duì)象是這個(gè)樣子的:
{
type: ADD_TODO,
payload: 'Learn Redux'
}
讓我們從reducer的總結(jié)轉(zhuǎn)到Redux-style的reducer:
const defaultState = 0;
const reducer = (state = defaultState, action) => {
switch (action.type) {
case 'ADD': return state + action.payload;
default: return state;
}
};
現(xiàn)在我們可以應(yīng)用一下測(cè)試actions
const actions = [
{ type: 'ADD', payload: 0 },
{ type: 'ADD', payload: 1 },
{ type: 'ADD', payload: 2 }
];
const total = actions.reduce(reducer, 0); // 3
4.Reducers必須是純函數(shù)
為了獲得受控state可重復(fù)性,reducers必須是純函數(shù).沒(méi)有意外情況,一個(gè)純函數(shù):
- 給定一個(gè)相同的輸入,總是返回相同的輸出值
- 沒(méi)有異步操作
在Javascript中非常重要的一點(diǎn),傳遞進(jìn)函數(shù)的所有的非原始對(duì)象都是傳引用賦值(references).換句話說(shuō),如果你傳遞一個(gè)對(duì)象到函數(shù),這個(gè)函數(shù)對(duì)對(duì)象的屬性做出了改變, 函數(shù)外部的對(duì)象也跟著發(fā)生變化.這就是副作用(side effect).
如果不知道傳遞對(duì)象的整個(gè)歷史,你不可能知道函數(shù)調(diào)用的完整意義.這一點(diǎn)很不利于開(kāi)發(fā).
Reducers應(yīng)該返回一個(gè)新的對(duì)象.例如你可以這樣做OBject.assign({},state,{thingToChange}).
數(shù)組的參數(shù)也是引用賦值的,不能再使用.push()方法把新的items添加到一個(gè)數(shù)組.因?yàn)?code>.push()會(huì)突變一個(gè)操作,.pop(), .shift(), .unshift(), .reverse(), .splice() 這些方法都不行.
(譯注:突變的意思是對(duì)源頭都改變了,突變以后,源頭是什么樣子就沒(méi)有辦法看到了)
如果你想在使用數(shù)組的時(shí)候,安全一點(diǎn),可以使用 concat()來(lái)代替.push(). (譯注:在reducer中使用concat可以參見(jiàn)iReading app)
看看chat Reducer中的ADD_CHAT的例子:
const ADD_CHAT = 'CHAT::ADD_CHAT';
const defaultState = {
chatLog: [],
currentChat: {
id: 0,
msg: '',
user: 'Anonymous',
timeStamp: 1472322852680
}
};
const chatReducer = (state = defaultState, action = {}) => {
const { type, payload } = action;
switch (type) {
case ADD_CHAT:
return Object.assign({}, state, {
chatLog: state.chatLog.concat(payload)
});
default: return state;
}
};
如你所見(jiàn),新的對(duì)象使用Object.assign()來(lái)創(chuàng)建,添加項(xiàng)目到數(shù)組用concat()代替.push()方法.
就我個(gè)人來(lái)說(shuō),我不喜歡去擔(dān)心我的State的突變事故,所以之后我會(huì)和Redux一起使用immubable data APIs.如果我的state是immutable對(duì)象,根本就不用看代碼,就知道對(duì)象是不會(huì)發(fā)生突變事故的.之所以得出這個(gè)結(jié)論是因?yàn)楹鸵粋€(gè)小組工作之后,發(fā)現(xiàn)了事故性突變的bugs.
(譯注:immutable.js的運(yùn)行原理,或者state的原理,其實(shí)參考版本庫(kù)的管理方法,每次的修改都會(huì)有一個(gè)唯一的標(biāo)示來(lái)記錄增刪改查的內(nèi)容)
還有很多的純函數(shù).如果你在app編程中使用Redux,你需要很好的掌握純函數(shù),其他事情你需要放在心上(例如:處理時(shí)間,日志和隨機(jī)數(shù)).
了解更多內(nèi)容請(qǐng)看Master the Javascript Interview:What is Pure Function.
5. 記住:Reducers一定是所有事實(shí)的唯一來(lái)源
在你的app中,所有的state應(yīng)該只有唯一的真相來(lái)源,意思是說(shuō):state存儲(chǔ)只存儲(chǔ)在一個(gè)地方,其他任何需要state的地方,都需要獲取state的引用賦值.
不同的事情有不同的來(lái)源也可以.例如,URL可以是用戶請(qǐng)求路徑和請(qǐng)求路徑的唯一來(lái)源.或許你的app有一個(gè)配置服務(wù),有API URLs的所有內(nèi)容.這也可以,但是...
當(dāng)你在Redux store中存儲(chǔ)state時(shí),接入到state,都需要通過(guò)Redux.不遵循這個(gè)原則可能會(huì)導(dǎo)致臟數(shù)據(jù)或者某種共享式的state 突變bug.
換句話,如果沒(méi)有單一來(lái)源的原則,你可能會(huì)丟失:
- 受控的視圖渲染
- 受控的state重現(xiàn)
- 簡(jiǎn)單易行的undo/redo
- 時(shí)間旅行degugging
- 容易實(shí)施的測(cè)試
要么用Redux管理的store,要么不用.如果有的地方用,有的地方不用,可能就會(huì)抵消使用Redux的好處.
6.為Action Types使用常量
我習(xí)慣于確保當(dāng)你查看action的歷史時(shí),很容易追蹤到使用他們的reducer.如果你的actionsde名字比較短的普通名字例如CHANGE_MESSAGE,會(huì)變得很難理解他在app中做什么.如果你的action types有更多描述性的名字例如:CHAT::CHANGE_MESSAGE,顯然很清楚要做什么.
如果你做了一個(gè)錯(cuò)誤的輸入,派發(fā)了一個(gè)沒(méi)有定義的action 常量,app將會(huì)拋出一個(gè)異常警告你錯(cuò)誤.如果會(huì)你輸錯(cuò)了action的累心字符串,action將不會(huì)顯示報(bào)錯(cuò)信息而失敗.
把所有的action type收集在一個(gè)文件的頂端可以幫助你:
- 保持名字的統(tǒng)一
- 快速理解reducer的API
- 理解請(qǐng)求中的變化
7.使用Action Creators從派發(fā)調(diào)用中解耦A(yù)ction的邏輯
當(dāng)我告訴其他人他們沒(méi)有生成IDS或者在reducer中獲取當(dāng)前時(shí)間,我看到的是滑稽的表情.如果你現(xiàn)在盯著屏幕感到疑惑:你也不是唯一這么想的.
有沒(méi)有一個(gè)好的地方來(lái)處理純邏輯,不要在需要使用action的地方重復(fù)他們?有,請(qǐng)使用action creator.
Action creators有其他的好處:
- 把a(bǔ)ction type常量封裝在reducer文件中,不能在其他地方導(dǎo)入.
- 在派發(fā)action之前對(duì)輸入做一些計(jì)算
- Reduce模板
讓我們來(lái)使用一個(gè)action creator 生成一個(gè)ADD_CHATaction 對(duì)象:
// Action creators can be impure.
export const addChat = ({
// cuid is safer than random uuids/v4 GUIDs
// see usecuid.org
id = cuid(),
msg = '',
user = 'Anonymous',
timeStamp = Date.now()
} = {}) => ({
type: ADD_CHAT,
payload: { id, msg, user, timeStamp }
});
如你所見(jiàn),我們使用cuid為每個(gè)聊天信息生成隨機(jī)的ids,使用Date.Now()生成時(shí)間戳.兩者都是純操作,在reducer中運(yùn)行不太安全.但是在action creatros中運(yùn)行時(shí)可以的.
使用Action Creators來(lái)減少模板代碼使用
一些程序員認(rèn)為是使用action creators添加模板到項(xiàng)目中.相反的,你將會(huì)看到我怎么用他們來(lái)大幅度在reducer中減少模板.
提示: 如果你把常量,reducer和action creators放到一個(gè)文件中,當(dāng)你需要從不同的路徑導(dǎo)入他們的時(shí)候,你就可以減少模板的需求.
想象著,我們需要給聊天用戶添加定制他們的用戶姓名和可用狀態(tài)的能力.我們可能會(huì)像下面一樣天界一列的action type:
const chatReducer = (state = defaultState, action = {}) => {
const { type, payload } = action;
switch (type) {
case ADD_CHAT:
return Object.assign({}, state, {
chatLog: state.chatLog.concat(payload)
});
case CHANGE_STATUS:
return Object.assign({}, state, {
statusMessage: payload
});
case CHANGE_USERNAME:
return Object.assign({}, state, {
userName: payload
});
default: return state;
}
};
對(duì)于大多數(shù)的reducers,這可能會(huì)增加一些模板代碼.很多我需要構(gòu)建的reducer比這個(gè)更復(fù)雜,他們有一些冗余的代碼.如果我要把這些簡(jiǎn)單的屬性改變action融合到一起怎么樣?
事實(shí)是,很容易:
const chatReducer = (state = defaultState, action = {}) => {
const { type, payload } = action;
switch (type) {
case ADD_CHAT:
return Object.assign({}, state, {
chatLog: state.chatLog.concat(payload)
});
// Catch all simple changes
case CHANGE_STATUS:
case CHANGE_USERNAME:
return Object.assign({}, state, payload);
default: return state;
}
};
即使使用額外的空格和注釋,這個(gè)版本也是很短的,這僅僅是兩個(gè)例子.action越多,代碼減少的越多.
switch… case安全嗎?我看到飛流直下的瀑布!(譯注:琢磨了兩天,才明白作者是這個(gè)意思,原文-I see a fall through!)
你可能在其他地方讀到過(guò)switch聲明應(yīng)該被避免,尤其是要避免偶然出現(xiàn)的瀑布一樣的流程,因?yàn)閏ases的列表會(huì)變得很臃腫.可能你聽(tīng)說(shuō)過(guò)了,不要刻意的使用瀑布式的代碼,因?yàn)椴东@瀑布流的bug非常難.這是個(gè)不錯(cuò)的建議,那就讓我們仔細(xì)考慮一下上面提到的危險(xiǎn).
- Reducers是可以組合的,所以case的臃腫不是問(wèn)題,如果case的列表變的很大,打碎成片段轉(zhuǎn)移到分離的reducers中.
- 每一個(gè)case體都會(huì)返回一個(gè)對(duì)象,如此一來(lái)瀑布流就不會(huì)出現(xiàn)了.一個(gè)瀑布流不應(yīng)該出現(xiàn)一個(gè)以上的異常捕獲語(yǔ)句
Redux使用switch..case.只要你遵循簡(jiǎn)單原則(保持switches語(yǔ)句體積小,目標(biāo)集中,在每個(gè)case都盡早的返回).swith語(yǔ)句是非常好的.
你可能注意到這個(gè)版本需要一個(gè)不同籌載(payload).這里就是你的action Creator的發(fā)源地
export const changeStatus = (statusMessage = 'Online') => ({
type: CHANGE_STATUS,
payload: { statusMessage }
});
export const changeUserName = (userName = 'Anonymous') => ({
type: CHANGE_USERNAME,
payload: { userName }
});
如你所見(jiàn),這些action creators 把參數(shù)和state的結(jié)構(gòu)改變聯(lián)系起來(lái)了,他是作為一個(gè)翻譯的角色.
8.在文檔中使用ES6的參數(shù)默認(rèn)值
如果你正在編輯器中使用Tern.js插件,他將會(huì)讀取這些ES6的默認(rèn)值,在你的action creators中需要的時(shí)候引用他們,所以當(dāng)你調(diào)用他們的時(shí)候,可以感知他們和執(zhí)行自動(dòng)完成.這會(huì)減少程序員的認(rèn)知負(fù)擔(dān),因?yàn)樗麄儾恍枰涀∷械妮d荷雷翔或者檢查他們記不起來(lái)的源代碼.
如果你沒(méi)有使用類型應(yīng)用插件例如:Tern,TypeScript或者Flow,是時(shí)候使用他們了.
//這一部分實(shí)在是不會(huì)翻譯了,留下來(lái)
Note: I prefer to rely on inference provided by default assignments visible in the function signature as opposed to type annotations, because:
You don’t have to use a Flow or TypeScript to make it work: Instead you use standard JavaScript.
If you are using TypeScript or Flow, annotations are redundant with default assignments, because both TypeScript and Flow infer the type from the default assignment.
I find it a lot more readable when there’s less syntax noise.
You get default settings, which means, even if you’re not stopping the CI build on type errors (you’d be surprised, lots of projects don’t), you’ll never have an accidental `undefined` parameter lurking in your code.
9. 使用Selectors來(lái)計(jì)算和解耦和State.
設(shè)想你正在構(gòu)建歷史上最復(fù)雜的聊天app.你已經(jīng)寫(xiě)了500K的代碼,然后產(chǎn)品團(tuán)隊(duì)拋出一個(gè)需要你必須改變state數(shù)據(jù)結(jié)構(gòu)的新需求.
不要痛苦,你可以很靈巧的使用selectors來(lái)從整個(gè)State中解耦和app的其余部分.子彈:躲開(kāi)

對(duì)于我寫(xiě)過(guò)的幾乎每一個(gè)reducer,我都創(chuàng)建了一個(gè)selector簡(jiǎn)單的輸出我需要在視圖中構(gòu)建的所需要的變量.讓我們看看簡(jiǎn)單的chat reducer
export const getViewState = state => Object.assign({}, state);
是的.我知道太簡(jiǎn)單了,不值得一看.你可能認(rèn)為我現(xiàn)在瘋了,但是記起了我們之前多個(gè)的子彈了嗎?如果我想添加一下計(jì)算state,例如所有會(huì)話中我交談過(guò)的用戶的完整列表?讓我們叫做recentlyActiveUsers.
這個(gè)信息已經(jīng)存儲(chǔ)在我們當(dāng)前的state中-但是不太容易得到.讓我們往前看,在getViewState()中獲取他.
export const getViewState = state => Object.assign({}, state, {
// return a list of users active during this session
recentlyActiveUsers: [...new Set(state.chatLog.map(chat => chat.user))]
});
如果你把所有的計(jì)算state都放在selector中,你:
- 減少了reducers和組件的復(fù)雜想
- 把你的app從state的結(jié)構(gòu)中解耦出來(lái).
- 遵守單一來(lái)源原則,甚至是在reducer中也是這樣.
10.使用TDD:測(cè)試優(yōu)先
很多研究比較了編寫(xiě)之前測(cè)試和編寫(xiě)之后測(cè)試的方法以及根本不測(cè)試的方法.結(jié)論是清除和顯著的,在實(shí)施編寫(xiě)之前,測(cè)試可以減少40-80%的bug.
TDD can effectively cut your shipping bug density in half, and there’s plenty of evidence to back up that claim.
在寫(xiě)這個(gè)文章中的示例時(shí),我都以單元測(cè)試開(kāi)始.
為了避免碎片化測(cè)試,我創(chuàng)建了如下的工廠來(lái)生產(chǎn)expections:
const createChat = ({
id = 0,
msg = '',
user = 'Anonymous',
timeStamp = 1472322852680
} = {}) => ({
id, msg, user, timeStamp
});
const createState = ({
userName = 'Anonymous',
chatLog = [],
statusMessage = 'Online',
currentChat = createChat()
} = {}) => ({
userName, chatLog, statusMessage, currentChat
});
** 注意這兩個(gè)測(cè)試我都使用了默認(rèn)值,意思是我可以越過(guò)屬性,單獨(dú)為我感興趣的測(cè)試提供數(shù)據(jù)**
——
這里是我使用的:
describe('chatReducer()', ({ test }) => {
test('with no arguments', ({ same, end }) => {
const msg = 'should return correct default state';
const actual = reducer();
const expected = createState();
same(actual, expected, msg);
end();
});
});
Note: 我使用tape來(lái)進(jìn)項(xiàng)單元測(cè)試,因?yàn)樗麎蚝?jiǎn)單.我也有2-3年使用Mocha和Jasmine的經(jīng)驗(yàn),以及其他的框架的零散經(jīng)驗(yàn).你需要根據(jù)這些原則找到合適的測(cè)試框架
注意我在測(cè)試標(biāo)書(shū)巢式測(cè)試時(shí)使用的風(fēng)格.可能由于我使用過(guò)Jasmine和Mocha框架的背景,我喜歡由外部代碼塊開(kāi)始描述需要測(cè)試的組件,接著才是內(nèi)部的代碼塊.在測(cè)試代碼塊內(nèi)部,我使用簡(jiǎn)單的相等斷言,也就是你的測(cè)試框架中的deepEqual()或者toEqual()函數(shù).
如你所見(jiàn),我使用分離的測(cè)試聲明和工廠函數(shù)來(lái)代替像beforeEach和afterEach()這樣的工具,這么工具誘導(dǎo)沒(méi)有經(jīng)驗(yàn)的開(kāi)發(fā)者在測(cè)試組件中使用共享的state來(lái)完成測(cè)試(這個(gè)做法不太好).
可能你會(huì)猜到,我已經(jīng)為每個(gè)reducer準(zhǔn)備了三種不同的測(cè)試:
- 直接的reducer測(cè)試,你可以在例子中看到.這些方法簡(jiǎn)單的測(cè)試reducer能否產(chǎn)生預(yù)期的默認(rèn)state.
- Action creator測(cè)試,通過(guò)使用預(yù)先設(shè)定好的sate作為起始點(diǎn),對(duì)每一個(gè)action應(yīng)用reducer來(lái)測(cè)試action的功能
- Selectors測(cè)試,測(cè)試每個(gè)selectors,確保每個(gè)預(yù)期的屬性的預(yù)期值都存在,包括經(jīng)過(guò)計(jì)算的屬性.
你已經(jīng)看到了一個(gè)reducer測(cè)試,讓我們看看其他的例子
Action Creators Test:
describe('addChat()', ({ test }) => {
test('with no arguments', ({ same, end}) => {
const msg = 'should add default chat message';
const actual = pipe(
() => reducer(undefined, addChat()),
// make sure the id and timestamp are there,
// but we don't care about the values
state => {
const chat = state.chatLog[0];
chat.id = !!chat.id;
chat.timeStamp = !!chat.timeStamp;
return state;
}
)();
const expected = Object.assign(createState(), {
chatLog: [{
id: true,
user: 'Anonymous',
msg: '',
timeStamp: true
}]
});
same(actual, expected, msg);
end();
});
test('with all arguments', ({ same, end}) => {
const msg = 'should add correct chat message';
const actual = reducer(undefined, addChat({
id: 1,
user: '@JS_Cheerleader',
msg: 'Yay!',
timeStamp: 1472322852682
}));
const expected = Object.assign(createState(), {
chatLog: [{
id: 1,
user: '@JS_Cheerleader',
msg: 'Yay!',
timeStamp: 1472322852682
}]
});
same(actual, expected, msg);
end();
});
});
這個(gè)例子非常的有意思,原因有幾個(gè).addChat() action creator是不純的.意思是除非你傳遞值代替原值,否則你就獲得不了預(yù)期的屬性值.為了對(duì)付這個(gè)問(wèn)題,我使用了管道.我有時(shí)使用管道來(lái)避免創(chuàng)建了不需要的附加值.又是使用它來(lái)忽略生成的值.我仍然卻行他們是存在的,但是我不關(guān)心這些值到底是什么.注意我甚至都沒(méi)有檢查type類型.我依靠類型引用和默認(rèn)值來(lái)完成和這個(gè)任務(wù).
一個(gè)管道(pipe)是一個(gè)工具函數(shù),讓你通過(guò)一系列的函數(shù)傳遞一些輸入值,這些系列函數(shù)都接受之前函數(shù)的輸出值,之后做出某種程度的變化.我使用了loadh/fp/pipe,別名是loadsh/flow.有意思的是pipe()也可以在reducer函數(shù)中創(chuàng)建.
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const fn1 = s => s.toLowerCase();
const fn2 = s => s.split('').reverse().join('');
const fn3 = s => s + '!'
const newFunc = pipe(fn1, fn2, fn3);
const result = newFunc('Time'); // emit!
我更愿意在reducers文件中使用pipe()來(lái)簡(jiǎn)化state的變化.所有的state的變化最終都從一個(gè)數(shù)數(shù)據(jù)流都另個(gè)數(shù)據(jù)流.pipe()很擅長(zhǎng)這個(gè)工作.
注意,action creator讓我們忽略所有的默認(rèn)值,所以我們可以傳遞特定的ids和時(shí)間戳,可以測(cè)試特殊的值.
Selectors測(cè)試
最后,我們來(lái)測(cè)試一下state selectors.確保經(jīng)過(guò)計(jì)算的值是正確的.要做的是:
describe('getViewState', ({ test }) => {
test('with chats', ({ same, end }) => {
const msg = 'should return the state needed to render';
const chats = [
createChat({
id: 2,
user: 'Bender',
msg: 'Does Barry Manilow know you raid his wardrobe?',
timeStamp: 451671300000
}),
createChat({
id: 2,
user: 'Andrew',
msg: `Hey let's watch the mouth, huh?`,
timeStamp: 451671480000 }),
createChat({
id: 1,
user: 'Brian',
msg: `We accept the fact that we had to sacrifice a whole Saturday in
detention for whatever it was that we did wrong.`,
timeStamp: 451692000000
})
];
const state = chats.map(addChat).reduce(reducer, reducer());
const actual = getViewState(state);
const expected = Object.assign(createState(), {
chatLog: chats,
recentlyActiveUsers: ['Bender', 'Andrew', 'Brian']
});
same(actual, expected, msg);
end();
});
});
注意,在這個(gè)測(cè)試中,我們使用了JS數(shù)組原型方法reducer()來(lái)reducer 累積一些actions addChat()的值.Redux reducer非常好的一個(gè)地方是,他們僅調(diào)控reducer函數(shù),你可以使用reducer做任何其他reducer能做的事情.
我們的expected值檢查了所有日志中的chat對(duì)象以及最近的活躍用戶的列表是否正確.
沒(méi)有什么要說(shuō)的了.
Redux的軍規(guī)
如果你正確的使用Redux,你會(huì)獲得很多大好處:
- 減少時(shí)間依賴的bugs
- 確定性的視圖渲染
- 確定性的state重演
- 容易實(shí)施的undo/redo特性
- 簡(jiǎn)單的debug
- 成為一個(gè)時(shí)間旅行者
但是為了保證上面的功能可以正常工作,你還要記住以下的規(guī)則:
- Reducer必須是純函數(shù)
- Reducer必須是state的唯一來(lái)源
- Reducer的state應(yīng)該總是被序列化
- Reducer state不能包含有函數(shù)
還要牢記于心:
- 不是所有的app都需要Redux
- 用常量定義action Types
- 使用action creators來(lái)解耦 action邏輯和dispatch的調(diào)用
- 使用ES6的默認(rèn)參數(shù)方法來(lái)描述參數(shù)特征
- 使用selectors來(lái)計(jì)算state和解耦
- 一定要使用TDD(譯注:馬上回考慮的)
祝你愉快!
譯文完
媽呀,5000多字,手指都敲掉皮了.沒(méi)功勞也有苦勞啊,看到這里給個(gè)??吧.
Members of “Learn JavaScript with Eric Elliott”, check out the new functional programming and Redux lessons. Be sure to watch the Shotgun series & ride shotgun with me while I build real apps with React and Redux.
Not a member? Join Today!
Eric Elliott is the author of “Programming JavaScript Applications” (O’Reilly), and “Learn JavaScript with Eric Elliott”. He has contributed to software experiences for Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC, and top recording artists including Usher, Frank Ocean, Metallica, and many more.
He spends most of his time in the San Francisco Bay Area with the most beautiful woman in the world.