Redux从设计到源码
本文主要講述三方面內(nèi)容:
Redux 背后的設(shè)計(jì)思想
源碼分析以及自定義中間件
開發(fā)中的最佳實(shí)踐
在講設(shè)計(jì)思想前,先簡單講下Redux是什么?我們?yōu)槭裁匆肦edux?
Redux是什么?
Redux是JavaScript狀態(tài)容器,能提供可預(yù)測化的狀態(tài)管理。
它認(rèn)為:
Web應(yīng)用是一個(gè)狀態(tài)機(jī),視圖與狀態(tài)是一一對應(yīng)的。
所有的狀態(tài),保存在一個(gè)對象里面。
我們先來看看“狀態(tài)容器”、“視圖與狀態(tài)一一對應(yīng)”以及“一個(gè)對象”這三個(gè)概念的具體體現(xiàn)。
如上圖,Store是Redux中的狀態(tài)容器,它里面存儲(chǔ)著所有的狀態(tài)數(shù)據(jù),每個(gè)狀態(tài)都跟一個(gè)視圖一一對應(yīng)。
Redux也規(guī)定,一個(gè)State對應(yīng)一個(gè)View。只要State相同,View就相同,知道了State,就知道View是什么樣,反之亦然。
比如,當(dāng)前頁面分三種狀態(tài):loading(加載中)、success(加載成功)或者error(加載失敗),那么這三個(gè)就分別唯一對應(yīng)著一種視圖。
現(xiàn)在我們對“狀態(tài)容器”以及“視圖與狀態(tài)一一對應(yīng)”有所了解了,那么Redux是怎么實(shí)現(xiàn)可預(yù)測化的呢?我們再來看下Redux的工作流程。
首先,我們看下幾個(gè)核心概念:
Store:保存數(shù)據(jù)的地方,你可以把它看成一個(gè)容器,整個(gè)應(yīng)用只能有一個(gè)Store。
State:Store對象包含所有數(shù)據(jù),如果想得到某個(gè)時(shí)點(diǎn)的數(shù)據(jù),就要對Store生成快照,這種時(shí)點(diǎn)的數(shù)據(jù)集合,就叫做State。
Action:State的變化,會(huì)導(dǎo)致View的變化。但是,用戶接觸不到State,只能接觸到View。所以,State的變化必須是View導(dǎo)致的。Action就是View發(fā)出的通知,表示State應(yīng)該要發(fā)生變化了。
Action Creator:View要發(fā)送多少種消息,就會(huì)有多少種Action。如果都手寫,會(huì)很麻煩,所以我們定義一個(gè)函數(shù)來生成Action,這個(gè)函數(shù)就叫Action Creator。
Reducer:Store收到Action以后,必須給出一個(gè)新的State,這樣View才會(huì)發(fā)生變化。這種State的計(jì)算過程就叫做Reducer。Reducer是一個(gè)函數(shù),它接受Action和當(dāng)前State作為參數(shù),返回一個(gè)新的State。
dispatch:是View發(fā)出Action的唯一方法。
然后我們過下整個(gè)工作流程:
首先,用戶(通過View)發(fā)出Action,發(fā)出方式就用到了dispatch方法。
然后,Store自動(dòng)調(diào)用Reducer,并且傳入兩個(gè)參數(shù):當(dāng)前State和收到的Action,Reducer會(huì)返回新的State
State一旦有變化,Store就會(huì)調(diào)用監(jiān)聽函數(shù),來更新View。
到這兒為止,一次用戶交互流程結(jié)束。可以看到,在整個(gè)流程中數(shù)據(jù)都是單向流動(dòng)的,這種方式保證了流程的清晰。
為什么要用Redux?
前端復(fù)雜性的根本原因是大量無規(guī)律的交互和異步操作。
變化和異步操作的相同作用都是改變了當(dāng)前View的狀態(tài),但是它們的無規(guī)律性導(dǎo)致了前端的復(fù)雜,而且隨著代碼量越來越大,我們要維護(hù)的狀態(tài)也越來越多。
我們很容易就對這些狀態(tài)何時(shí)發(fā)生、為什么發(fā)生以及怎么發(fā)生的失去控制。那么怎樣才能讓這些狀態(tài)變化能被我們預(yù)先掌握,可以復(fù)制追蹤呢?
這就是Redux設(shè)計(jì)的動(dòng)機(jī)所在。
Redux試圖讓每個(gè)State變化都是可預(yù)測的,將應(yīng)用中所有的動(dòng)作與狀態(tài)都統(tǒng)一管理,讓一切有據(jù)可循。
如上圖所示,如果我們的頁面比較復(fù)雜,又沒有用任何數(shù)據(jù)層框架的話,就是圖片上這個(gè)樣子:交互上存在父子、子父、兄弟組件間通信,數(shù)據(jù)也存在跨層、反向的數(shù)據(jù)流。
這樣的話,我們維護(hù)起來就會(huì)特別困難,那么我們理想的應(yīng)用狀態(tài)是什么樣呢?看下圖:
架構(gòu)層面上講,我們希望UI跟數(shù)據(jù)和邏輯分離,UI只負(fù)責(zé)渲染,業(yè)務(wù)和邏輯交由其它部分處理,從數(shù)據(jù)流向方面來說, 單向數(shù)據(jù)流確保了整個(gè)流程清晰。
我們之前的操作可以復(fù)制、追蹤出來,這也是Redux的主要設(shè)計(jì)思想。
綜上,Redux可以做到:
- 每個(gè)State變化可預(yù)測。
- 動(dòng)作與狀態(tài)統(tǒng)一管理。
Redux思想追溯
Redux作者在Redux.js官方文檔Motivation一章的最后一段明確提到:
Following in the steps of Flux, CQRS, and Event Sourcing , Redux attempts to make state mutations predictable by imposing certain restrictions on how and when updates can happen.
我們就先了解下Flux、CQRS、ES(Event Sourcing 事件溯源)這幾個(gè)概念。
什么是ES?
不是保存對象的最新狀態(tài),而是保存對象產(chǎn)生的事件。
通過事件追溯得到對象最新狀態(tài)。
舉個(gè)例子:我們平常記賬有兩種方式,直接記錄每次賬單的結(jié)果或者記錄每次的收入/支出,那么我們自己計(jì)算的話也可以得到結(jié)果,ES就是后者。
與傳統(tǒng)增刪改查關(guān)系式存儲(chǔ)的區(qū)別:
傳統(tǒng)的增刪是以結(jié)果為導(dǎo)向的數(shù)據(jù)存儲(chǔ),ES是以過程為導(dǎo)向存儲(chǔ)。
CRUD是直接對庫進(jìn)行操作。
ES是在庫里存了一系列事件的集合,不直接對庫里記錄進(jìn)行更改。
優(yōu)點(diǎn):
高性能:事件是不可更改的,存儲(chǔ)的時(shí)候并且只做插入操作,也可以設(shè)計(jì)成獨(dú)立、簡單的對象。所以存儲(chǔ)事件的成本較低且效率較高,擴(kuò)展起來也非常方便。
簡化存儲(chǔ):事件用于描述系統(tǒng)內(nèi)發(fā)生的事情,我們可以考慮用事件存儲(chǔ)代替復(fù)雜的關(guān)系存儲(chǔ)。
溯源:正因?yàn)槭录遣豢筛牡?#xff0c;并且記錄了所有系統(tǒng)內(nèi)發(fā)生的事情,我們能用它來跟蹤問題、重現(xiàn)錯(cuò)誤,甚至做備份和還原。
缺點(diǎn):
事件丟失:因?yàn)镋S存儲(chǔ)都是基于事件的,所以一旦事件丟失就很難保證數(shù)據(jù)的完整性。
修改時(shí)必須兼容老結(jié)構(gòu):指的是因?yàn)槔系氖录豢勺?#xff0c;所以當(dāng)業(yè)務(wù)變動(dòng)的時(shí)候新的事件必須兼容老結(jié)構(gòu)。
CQRS(Command Query Responsibility Segregation)是什么?
顧名思義,“命令與查詢職責(zé)分離”–>”讀寫分離”。
整體的思想是把Query操作和Command操作分成兩塊獨(dú)立的庫來維護(hù),當(dāng)事件庫有更新時(shí),再來同步讀取數(shù)據(jù)庫。
看下Query端,只是對數(shù)據(jù)庫的簡單讀操作。然后Command端,是對事件進(jìn)行簡單的存儲(chǔ),同時(shí)通知Query端進(jìn)行數(shù)據(jù)更新,這個(gè)地方就用到了ES。
優(yōu)點(diǎn):
- CQ兩端分離,各自獨(dú)立。
- 技術(shù)代碼和業(yè)務(wù)代碼完全分離。
缺點(diǎn):
- 強(qiáng)依賴高性能可靠的分布式消息隊(duì)列。
Flux是什么?
Flux是一種架構(gòu)思想,下面過程中,數(shù)據(jù)總是“單向流動(dòng)”,任何相鄰的部分都不會(huì)發(fā)生數(shù)據(jù)的“雙向流動(dòng)”,這保證了流程的清晰。Flux的最大特點(diǎn),就是數(shù)據(jù)的“單向流動(dòng)”。
介紹完以上之后,我們來整體做一下對比。
CQRS與Flux
相同:當(dāng)數(shù)據(jù)在write side發(fā)生更改時(shí),一個(gè)更新事件會(huì)被推送到read side,通過綁定事件的回調(diào),read side得知數(shù)據(jù)已更新,可以選擇是否重新讀取數(shù)據(jù)。
差異:在CQRS中,write side和read side分屬于兩個(gè)不同的領(lǐng)域模式,各自的邏輯封裝和隔離在各自的Model中,而在Flux里,業(yè)務(wù)邏輯都統(tǒng)一封裝在Store中。
Redux與Flux
Redux是Flux思想的一種實(shí)現(xiàn),同時(shí)又在其基礎(chǔ)上做了改進(jìn)。Redux還是秉承了Flux單向數(shù)據(jù)流、Store是唯一的數(shù)據(jù)源的思想。
最大的區(qū)別:
Flux中允許有多個(gè)Store,但是Redux中只允許有一個(gè),相較于Flux,一個(gè)Store更加清晰,容易管理。Flux里面會(huì)有多個(gè)Store存儲(chǔ)應(yīng)用數(shù)據(jù),并在Store里面執(zhí)行更新邏輯,當(dāng)Store變化的時(shí)候再通知controller-view更新自己的數(shù)據(jù);Redux將各個(gè)Store整合成一個(gè)完整的Store,并且可以根據(jù)這個(gè)Store推導(dǎo)出應(yīng)用完整的State。
同時(shí)Redux中更新的邏輯也不在Store中執(zhí)行而是放在Reducer中。單一Store帶來的好處是,所有數(shù)據(jù)結(jié)果集中化,操作時(shí)的便利,只要把它傳給最外層組件,那么內(nèi)層組件就不需要維持State,全部經(jīng)父級(jí)由props往下傳即可。子組件變得異常簡單。
Redux去除了這個(gè)Dispatcher,使用Store的Store.dispatch()方法來把a(bǔ)ction傳給Store,由于所有的action處理都會(huì)經(jīng)過這個(gè)Store.dispatch()方法,Redux聰明地利用這一點(diǎn),實(shí)現(xiàn)了與Koa、RubyRack類似的Middleware機(jī)制。Middleware可以讓你在dispatch action后,到達(dá)Store前這一段攔截并插入代碼,可以任意操作action和Store。很容易實(shí)現(xiàn)靈活的日志打印、錯(cuò)誤收集、API請求、路由等操作。
除了以上,Redux相對Flux而言還有以下特性和優(yōu)點(diǎn):
文檔清晰,編碼統(tǒng)一。
逆天的DevTools,可以讓應(yīng)用像錄像機(jī)一樣反復(fù)錄制和重放。
目前,美團(tuán)外賣后端管理平臺(tái)的上單各個(gè)模塊已經(jīng)逐步替換為React+Redux開發(fā)模式,流程的清晰為錯(cuò)誤追溯和代碼維護(hù)提供了便利,現(xiàn)實(shí)工作中也大大提高了人效。
查看源碼的話先從GitHub把這個(gè)地址上拷下來,切換到src目錄,如下圖:
看下整體結(jié)構(gòu):
其中utils下面的Warning.js主要負(fù)責(zé)控制臺(tái)錯(cuò)誤日志的輸出,我們直接忽略index.js是入口文件,createStore.js是主流程文件,其余4個(gè)文件都是輔助性的API。
我們先結(jié)合下流程分析下對應(yīng)的源碼。
首先,我們從Redux中引入createStore方法,然后調(diào)用createStore方法,并將Reducer作為參數(shù)傳入,用來生成Store。為了接收到對應(yīng)的State更新,我們先執(zhí)行Store的subscribe方法,將render作為監(jiān)聽函數(shù)傳入。然后我們就可以dispatchaction了,對應(yīng)更新view的State。
那么我們按照順序看下對應(yīng)的源碼:
入口文件index.js
入口文件,上面一堆檢測代碼忽略,看紅框標(biāo)出部分,它的主要作用相當(dāng)于提供了一些方法,這些方法也是Redux支持的所有方法。
然后我們看下主流程文件:createStore.js。
主流程文件:createStore.js
createStore主要用于Store的生成,我們先整理看下createStore具體做了哪些事兒。
首先,一大堆類型判斷先忽略,可以看到聲明了一系列函數(shù),然后執(zhí)行了dispatch方法,最后暴露了dispatch、subscribe……幾個(gè)方法。這里dispatch了一個(gè)init Action是為了生成初始的State樹。
我們先挑兩個(gè)簡單的函數(shù)看下,getState和replaceReducer,其中g(shù)etState只是返回了當(dāng)前的狀態(tài)。replaceReducer是替換了當(dāng)前的Reducer并重新初始化了State樹。這兩個(gè)方法比較簡單,下面我們在看下其它方法。
訂閱函數(shù)的主要作用是注冊監(jiān)聽事件,然后返回取消訂閱的函數(shù),它把所有的訂閱函數(shù)統(tǒng)一放一個(gè)數(shù)組里,只維護(hù)這個(gè)數(shù)組。
為了實(shí)現(xiàn)實(shí)時(shí)性,所以這里用了兩個(gè)數(shù)組來分別處理dispatch事件和接收subscribe事件。
store.subscribe()方法總結(jié):
入?yún)⒑瘮?shù)放入監(jiān)聽隊(duì)列
返回取消訂閱函數(shù)
再來看下store.dispatch()–>分發(fā)action,修改State的唯一方式。
store.dispatch()方法總結(jié):
調(diào)用Reducer,傳參(currentState,action)。
按順序執(zhí)行l(wèi)istener。
返回action。
到這兒的話,主流程我們就講完了,下面我們講下幾個(gè)輔助的源碼文件。
bindActionCreators.js
bindActionCreators把a(bǔ)ction creators轉(zhuǎn)成擁有同名keys的對象,使用dispatch把每個(gè)action creator包裝起來,這樣可以直接調(diào)用它們。
實(shí)際情況用到的并不多,惟一的應(yīng)用場景是當(dāng)你需要把a(bǔ)ction creator往下傳到一個(gè)組件上,卻不想讓這個(gè)組件覺察到Redux的存在,而且不希望把Redux Store或dispatch傳給它。
combineReducers.js–>用于合并Reducer
這個(gè)方法的主要功能是用來合并Reducer,因?yàn)楫?dāng)我們應(yīng)用比較大的時(shí)候Reducer按照模塊拆分看上去會(huì)比較清晰,但是傳入Store的Reducer必須是一個(gè)函數(shù),所以用這個(gè)方法來作合并。代碼不復(fù)雜,就不細(xì)講了。它的用法和最后的效果可以看下上面左側(cè)圖。
compose.js–>用于組合傳入的函數(shù)
compose這個(gè)方法,主要用來組合傳入的一系列函數(shù),在中間件時(shí)會(huì)用到。可以看到,執(zhí)行的最終結(jié)果是把各個(gè)函數(shù)串聯(lián)起來。
applyMiddleware.js–>用于Store增強(qiáng)
中間件是Redux源碼中比較繞的一部分,我們結(jié)合用法重點(diǎn)看下。
首先看下用法:
const store = createStore(reducer,applyMiddleware(…middlewares)) or const store = createStore(reducer,{},applyMiddleware(…middlewares))可以看到,是將中間件作為createStore的第二個(gè)或者第三個(gè)參數(shù)傳入,然后我們看下傳入之后實(shí)際發(fā)生了什么。
從代碼的最后一行可以看到,最后的執(zhí)行代碼相當(dāng)于applyMiddleware(…middlewares)(createStore)(reducer,preloadedState)然后我們?nèi)pplyMiddleware里看它的執(zhí)行過程。
可以看到執(zhí)行方法有三層,那么對應(yīng)我們源碼看的話最終會(huì)執(zhí)行最后一層。最后一層的執(zhí)行結(jié)果是返回了一個(gè)正常的Store和一個(gè)被變更過的dispatch方法,實(shí)現(xiàn)了對Store的增強(qiáng)。
這里假設(shè)我們傳入的數(shù)組chain是[f,g,h],那么我們的dispatch相當(dāng)于把原有dispatch方法進(jìn)行f,g,h層層過濾,變成了新的dispatch。
由此的話我們可以推出中間件的寫法:因?yàn)橹虚g件是要多個(gè)首尾相連的,需要一層層的“加工”,所以要有個(gè)next方法來獨(dú)立一層確保串聯(lián)執(zhí)行,另外dispatch增強(qiáng)后也是個(gè)dispatch方法,也要接收action參數(shù),所以最后一層肯定是action。
再者,中間件內(nèi)部需要用到Store的方法,所以Store我們放到頂層,最后的結(jié)果就是:
看下一個(gè)比較常用的中間件redux-thunk源碼,關(guān)鍵代碼只有不到10行。
作用的話可以看到,這里有個(gè)判斷:如果當(dāng)前action是個(gè)函數(shù)的話,return一個(gè)action執(zhí)行,參數(shù)有dispatch和getState,否則返回給下個(gè)中間件。
這種寫法就拓展了中間件的用法,讓action可以支持函數(shù)傳遞。
我們來總結(jié)下這里面的幾個(gè)疑點(diǎn)。
Q1:為什么要嵌套函數(shù)?為何不在一層函數(shù)中傳遞三個(gè)參數(shù),而要在一層函數(shù)中傳遞一個(gè)參數(shù),一共傳遞三層?
因?yàn)橹虚g件是要多個(gè)首尾相連的,對next進(jìn)行一層層的“加工”,所以next必須獨(dú)立一層。那么Store和action呢?Store的話,我們要在中間件頂層放上Store,因?yàn)槲覀円肧tore的dispatch和getState兩個(gè)方法。action的話,是因?yàn)槲覀兎庋b了這么多層,其實(shí)就是為了作出更高級(jí)的dispatch方法,是dispatch,就得接受action這個(gè)參數(shù)。
Q2:middlewareAPI中的dispatch為什么要用匿名函數(shù)包裹呢?
我們用applyMiddleware是為了改造dispatch的,所以applyMiddleware執(zhí)行完后,dispatch是變化了的,而middlewareAPI是applyMiddleware執(zhí)行中分發(fā)到各個(gè)middleware,所以必須用匿名函數(shù)包裹dispatch,這樣只要dispatch更新了,middlewareAPI中的dispatch應(yīng)用也會(huì)發(fā)生變化。
Q3: 在middleware里調(diào)用dispatch跟調(diào)用next一樣嗎?
因?yàn)槲覀兊膁ispatch是用匿名函數(shù)包裹,所以在中間件里執(zhí)行dispatch跟其它地方?jīng)]有任何差別,而執(zhí)行next相當(dāng)于調(diào)用下個(gè)中間件。
到這兒為止,源碼部分就介紹完了,下面總結(jié)下開發(fā)中的最佳實(shí)踐。
官網(wǎng)中對最佳實(shí)踐總結(jié)的很到位,我們重點(diǎn)總結(jié)下以下幾個(gè):
用對象展開符增加代碼可讀性。
區(qū)分smart component(know the State)和dump component(完全不需要關(guān)心State)。
component里不要出現(xiàn)任何async calls,交給action creator來做。
Reducer盡量簡單,復(fù)雜的交給action creator。
Reducer里return state的時(shí)候,不要改動(dòng)之前State,請返回新的。
immutable.js配合效果很好(但同時(shí)也會(huì)帶來強(qiáng)侵入性,可以結(jié)合實(shí)際項(xiàng)目考慮)。
action creator里,用promise/async/await以及Redux-thunk(redux-saga)來幫助你完成想要的功能。
action creators和Reducer請用pure函數(shù)。
請慎重選擇組件樹的哪一層使用connected component(連接到Store),通常是比較高層的組件用來和Store溝通,最低層組件使用這防止太長的prop chain。
請慎用自定義的Redux-middleware,錯(cuò)誤的配置可能會(huì)影響到其他middleware.
有些時(shí)候有些項(xiàng)目你并不需要Redux(畢竟引入Redux會(huì)增加一些額外的工作量)
瑩瑩,美團(tuán)外賣前端研發(fā)工程師,2016年加入美團(tuán)外賣,負(fù)責(zé)外賣商家管理平臺(tái)以及銷售人員App蜜蜂的整個(gè)上單流程開發(fā)。
最后,附上一條硬廣,美團(tuán)外賣長期誠聘高級(jí)前端工程師/前端技術(shù)專家,歡迎發(fā)送簡歷至:tianhuan02#meituan.com。
總結(jié)
以上是生活随笔為你收集整理的Redux从设计到源码的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 研发团队资源成本优化实践
- 下一篇: 阿里P8架构师谈:分布式系统全局唯一ID