原文地址在我的博客, 轉(zhuǎn)載請(qǐng)注明出處,謝謝!
概述
本文介紹了我對(duì) Redux 狀態(tài)管理的思想、原理、架構(gòu)方法的認(rèn)識(shí)和思考以及配合redux-saga處理異步操作的實(shí)踐
前言
You know, React 只是屬于MV*架構(gòu)模式的 view 層,是一種狀態(tài)機(jī),只使用 React 難以控制大型、復(fù)雜的應(yīng)用,它需要一些框架來(lái)幫助管理狀態(tài),因此如何有效、簡(jiǎn)單、易于測(cè)試地管理這個(gè)狀態(tài)機(jī)是各種架構(gòu)框架感興趣的。Facebook 早就意識(shí)到這個(gè)問(wèn)題并提出了 Flux 架構(gòu),比較復(fù)雜; 后來(lái)出現(xiàn)了 Redux、Mobx 等。MobX 可以處理簡(jiǎn)單數(shù)據(jù)流的場(chǎng)景,可以實(shí)現(xiàn)精確更新; Redux 是從 Flux 和其他框架借鑒了一些思想, 它比 Flux 簡(jiǎn)單、易于理解、用于處理復(fù)雜數(shù)據(jù)流,并具有很強(qiáng)的擴(kuò)展性,社區(qū)誕生了像redux-thunk、redux-promise、redux-saga等中間件用于方便地處理異步操作。最近也在項(xiàng)目中使用了 Redux 及其中間件 redux-saga 來(lái)管理狀態(tài)和處理異步操作,這篇文章就來(lái)談?wù)勎覍?duì)它們的思考和實(shí)踐。
正文
Redux 思想
先來(lái)談?wù)劚尘埃ㄐ枨螅?/strong>
我覺(jué)得理解Redux的思想,談?wù)凪V*架構(gòu)模式的思路也許會(huì)有幫助。
MV*架構(gòu)模式,它們的核心都是職責(zé)分離、解耦,不同的層次做不同的事,能讓一個(gè)復(fù)雜、混亂的應(yīng)用變得思路清晰,代碼可以復(fù)用,并且易于測(cè)試,有利于分工合作,構(gòu)建更大、更復(fù)雜的應(yīng)用。
一個(gè)應(yīng)用要包括哪些功能?表現(xiàn)(view)、處理數(shù)據(jù)的邏輯(model)以及數(shù)據(jù)映射到表現(xiàn)層的邏輯(presenter or controller),數(shù)據(jù)在這三層之間流通(MVVM模式通過(guò)數(shù)據(jù)雙向綁定實(shí)現(xiàn)view和model同步)。
React 根據(jù)state來(lái)render,它只是個(gè)狀態(tài)機(jī),并沒(méi)有解決管理狀態(tài)的問(wèn)題。我們?cè)趩渭兊氖褂肦eact來(lái)寫(xiě)組件的時(shí)候,經(jīng)常會(huì)遇到組件間通信和管理組件state的問(wèn)題,前者常用的解決辦法就是把數(shù)據(jù)提到父組件共享;后者管理state簡(jiǎn)單的情況還行,一復(fù)雜就很麻煩且容易出錯(cuò),再遇到一些需要異步處理的操作,想想就頭皮發(fā)麻。
當(dāng)你開(kāi)發(fā)中遇到一些反人類的操作時(shí),試著去想如何改變一下思路讓它變得更簡(jiǎn)單,別耐著性子安慰自己開(kāi)發(fā)就是這樣 :)
解決方案
Redux 正是用來(lái)解決大型React應(yīng)用所面臨的狀態(tài)管理、數(shù)據(jù)流通、異步處理、測(cè)試、團(tuán)隊(duì)合作等問(wèn)題:
Redux 用單一的object tree來(lái)表示整個(gè)應(yīng)用的state,這個(gè)表示state的對(duì)象樹(shù)被放在唯一的store 中,state相當(dāng)于store的快照;所有組件都會(huì)通過(guò)API拿到這個(gè)state,各取所需;
Redux 把頁(yè)面上用戶的操作或者瀏覽器的行為(如路由的變化)定義為一個(gè)要更新state的action,這個(gè)action是一個(gè)普通對(duì)象,它包含了要執(zhí)行動(dòng)作的類別和傳遞到state的數(shù)據(jù)(如果有的話),它只表明要更改state的意圖,相當(dāng)于一個(gè)信號(hào),并不能直接修改state,Redux會(huì)集中處理這些信號(hào),這個(gè)action由你來(lái)決定何時(shí)發(fā)起;
定義好信號(hào),你還需要根據(jù)不同的信號(hào)定義不同的邏輯函數(shù)(reducers)來(lái)更新state。
通過(guò)這張圖來(lái)整理一下:

咳咳...比如用戶點(diǎn)擊的一個(gè)按鈕,你在按鈕上綁定的回調(diào)函數(shù)調(diào)用了一個(gè)(多個(gè))action creator,action creator就返回了一個(gè)更新state局部數(shù)據(jù)的action,store會(huì)根據(jù)這個(gè)(多個(gè))action找到對(duì)應(yīng)的reducers(reducer需要做拆分),按照action發(fā)起的順序依次執(zhí)行來(lái)更新state,每個(gè)reducer只負(fù)責(zé)更新自己關(guān)心那部分,根 reducer 把多個(gè)子 reducer 輸出合并成一個(gè)單一的 state 樹(shù),生成一個(gè)新的state保存在store中,store中的state可以通過(guò)相應(yīng)API傳遞到子組件。
這就是整個(gè)數(shù)據(jù)流。
那Redux如何處理異步操作?
Redux借鑒了中間件思想,利用可擴(kuò)展的中間件來(lái)改造dispatch函數(shù)。比如redux-thunk讓dispatch不僅僅可以接收action,還可以接受函數(shù)作為參數(shù),你可以在這個(gè)函數(shù)里完成異步操作。再如redux-saga更強(qiáng)大、也更復(fù)雜,在后面會(huì)講到。
Redux 架構(gòu)方法
對(duì)于React技術(shù)棧,Redux實(shí)現(xiàn)了react-redux庫(kù)來(lái)讓Redux管理React應(yīng)用(其他框架也有相應(yīng)的庫(kù)),里面集成了一些有用的函數(shù)來(lái)把一些明確的流程自動(dòng)化,如createStore用于創(chuàng)建唯一store,可以把根reducer傳進(jìn)createStore使store自動(dòng)調(diào)用對(duì)應(yīng)reducer,可以擴(kuò)展中間件;提供<Provider store>組件和connect高階組件用來(lái)包裹render component并傳遞state,connect還能自動(dòng)dispatch,讓你只要調(diào)用action creator就能dispatch;提供combineReducers來(lái)組合分割的reducers等。
知道這些特性,就可以配合react-router構(gòu)建大型應(yīng)用了:
總的思路就是:利用react-router 把應(yīng)用分割為各個(gè)頁(yè)面,reducer、action creator也跟隨頁(yè)面分割而分割。每個(gè)路由對(duì)應(yīng)的頁(yè)面下都有components和containers,分別存放functional components 和class components,前者用來(lái)渲染,后者當(dāng)做containers被connect包裹,containers包裹c(diǎn)omponents;containers從connect得到state并映射需要的數(shù)據(jù)到子組件的props,子組件再向下傳遞。
具體如何構(gòu)建React + Redux + react-router,我在另一篇博客里講了。
使用這種架構(gòu),開(kāi)發(fā)大型應(yīng)用變得得心應(yīng)手。
Redux 存在的問(wèn)題
但是當(dāng)我深入項(xiàng)目開(kāi)發(fā)的時(shí)候,也逐漸發(fā)現(xiàn)了一些問(wèn)題:
- 這種架構(gòu)項(xiàng)目結(jié)構(gòu)不夠扁平化,文件嵌套比較深,思路比較復(fù)雜,搭建、寫(xiě)起來(lái)比較麻煩,上手有難度;
- 由于所有
actioncreator都定義在頁(yè)面層次上,讓子組件調(diào)用必須一層一層的傳遞,很麻煩且非常容易出錯(cuò),也很難調(diào)試; - state難以做到局部更新(這個(gè)可以用
reselect) - Redux只是傳遞了一種思路,定義了幾個(gè)簡(jiǎn)單的API,很靈活,架構(gòu)方式不固定,設(shè)計(jì)方式不固定(如:如何設(shè)計(jì)state樹(shù))但這也是它的缺點(diǎn),新人往往看完一遍還是不知道怎么做,對(duì)新人不友好
總之,redux可以勝任復(fù)雜數(shù)據(jù)流的應(yīng)用,但是也比較難,前期架構(gòu)比較麻煩,適合有經(jīng)驗(yàn)的人。
使用redux-saga處理異步操作
Redux 倡導(dǎo)action 和reducer盡可能"純凈",沒(méi)有什么“副作用”??墒窍褚恍┊惒讲僮鞅热绔@取數(shù)據(jù)是必須的,在哪處理這些副作用呢?redux 把這些"不純凈的"任務(wù)交給了中間件,通過(guò) 向createStore里應(yīng)用中間件,在交由store處理action之前就可以對(duì)其完成一些其他的操作:

而redux-saga 是Redux一個(gè)強(qiáng)大但并不復(fù)雜的用于異步處理的中間件。
它的思路是什么?相比其他redux異步中間件如redux-thunk、redux-promise有什么不同?
先看名字來(lái)理解:saga,這個(gè)術(shù)語(yǔ)常用于CQRS架構(gòu),代表查詢與責(zé)任分離。
沒(méi)錯(cuò),就是查詢(dispatch)與責(zé)任(sagas)分離。saga提供了action監(jiān)聽(tīng)函數(shù),只需在組件里dispatch 相應(yīng)type的action,就可以自動(dòng)調(diào)用你定義好的對(duì)應(yīng)這個(gè)action的異步處理函數(shù)(sagas)來(lái)完成任務(wù),保證了只在組件里dispatch action來(lái)發(fā)起異步操作而不是redux-thunk、redux-promise的調(diào)用action creators。
另外一大特色就是redux-saga做到了異步代碼以同步方式寫(xiě),非常直觀方便,怎么做到的呢?它是利用了ES6新魔法Generator迭代器,可以完美解決異步回調(diào)地獄,讓你以同步方式寫(xiě)異步。saga正是利用Generator特性讓其處理異步變得非常方便又容易理解。這是一個(gè)常見(jiàn)的請(qǐng)求后臺(tái)數(shù)據(jù)的異步操作,感受一下:
function *fetchNodeDetailByNodeId({ payload: { nodeId } }, { call, put }) {
try {
const { data, status }= yield call(fetchNodeDetailByNodeId, nodeId)
if (data && status.errmsg === 'success') {
yield put({
type: 'setStates',
payload: {
nodeDetailData: data,
},
});
} else {
message.info('開(kāi)了個(gè)小差,再試一次吧..');
}
} catch (error) {
console.log(error);
}
},
call 和 put 是saga的API,相當(dāng)于dispatch,但是并不是真正執(zhí)行dispatch,只是發(fā)送你指定的指令,交由saga中間件來(lái)執(zhí)行這個(gè)指令。這樣看來(lái),這個(gè)saga函數(shù)就是一些指令的集合,稱為effects,副作用,用來(lái)描述任務(wù)
為啥要描述指令而不直接調(diào)用呢?這樣是因?yàn)橐子跍y(cè)試,如果直接調(diào)用,你還得模擬調(diào)用的函數(shù),詳見(jiàn)redux-saga文檔。
我覺(jué)得redux-saga相比于其他中間件的優(yōu)點(diǎn):
- 查詢與責(zé)任分離,保證了
action的純潔性,符合redux設(shè)計(jì)思想 - 實(shí)現(xiàn)以同步方式寫(xiě)異步操作,容易理解,邏輯清晰
- 通過(guò)發(fā)送指令而不是直接調(diào)用讓異步操作變得容易測(cè)試
- 監(jiān)聽(tīng)、執(zhí)行自動(dòng)化
- 提供了豐富強(qiáng)大的指令來(lái)完成復(fù)雜的操作,比如無(wú)阻塞調(diào)用,同時(shí)執(zhí)行多個(gè)任務(wù)等
講道理,任何redux異步操作都可以讓saga這個(gè)中間件來(lái)完成,非常復(fù)雜的同樣可以勝任,并且很容易理解(異步操作以同步方式寫(xiě))和測(cè)試。再配合dva,可以減輕redux的復(fù)雜度同時(shí)完成更強(qiáng)大的功能。
這樣以來(lái),redux配合saga,就可以讓它們各司其職,整個(gè)思路也變得清晰起來(lái):
redux 倡導(dǎo)action和reducer要純潔,那就讓所有異步操作這些不純潔的任務(wù)交給saga,reducer不用變,還是純函數(shù);定義好對(duì)應(yīng)action的sagas專門(mén)用來(lái)處理異步操作,我只要在組件需要的地方里dispatch 純action就行了,符合redux設(shè)計(jì)思想。
總結(jié)
使用redux來(lái)管理應(yīng)用狀態(tài)適用于復(fù)雜的應(yīng)用,而復(fù)雜的應(yīng)用會(huì)有復(fù)雜的異步處理,異步處理不要用redux的action creator,它不是用來(lái)做這個(gè)的,也違背了redux設(shè)計(jì)思想,redux把這些任務(wù)交給了異步中間件,應(yīng)該由它們來(lái)完成。使用redux saga是一個(gè)推薦的選擇,它懂redux,也懂你需要什么。另外,既然你用到了saga,不妨試試dva架構(gòu),5分鐘上手,值得一試。