redux真的不復雜——原始碼解讀
閱讀物件:使用過redux,對redux實現原理不是很理解的開發者。
在我實習入職培訓的時候,我的前端組長就跟我說過,redux的核心原始碼很簡潔,建議我有空去看一下,提升對redux系列的理解。
入職一個多月了,已經參與了公司的不少專案,redux也使用了一段時間,對於redux的理解缺一直沒有深入,還停留在“知道怎麼用,但是不知道其核心原理”的階段。
所以就在github上拉了redux的原始碼,看了一會,發現東西卻是不多,比較簡潔。
redux本身的功能是什麼
在專案中,我們往往不會純粹的使用redux,而是會配合其他的一些工具庫提升效率,比如 react-redux
,讓react應用使用redux更容易,類似的也有 wepy-redux
,提供給小程式框架wepy的工具庫。
但是在本文中,我們討論的範圍就純粹些,僅僅討論 redux本身 。
redux本身有哪些作用?我們先來快速的過一下redux的核心思想(工作流程):
- 將狀態統一放在一個state中,由store來管理這個state
- 這個store由reducer建立,reducer的作用是接受之前的狀態,返回一個新的狀態。
- 外部改變state的唯一方法是通過呼叫store的dispatch方法,觸發一個action,這個action被對應的reducer處理,於是state完成更新。
- 可以通過subscribe在store上新增一個監聽函式,store中dispatch方法被呼叫時,會執行這個監聽函式。
- 可以新增中介軟體(中介軟體是幹什麼的我們後面講)
在這個工作流程中,redux需要提供的功能是:
createStore() combineReducers() applyMiddleware()
沒錯,就這麼多方法,我們看下redux的原始碼目錄:

卻是也就這麼多,至於其他的如compose,bindActionCreators都是一些工具方法。下面我們就逐個來看看其原始碼實現。
你可以在github上克隆原始碼到本地,我後面的分析你可以參照著原始碼看。
createStore的實現
這個函式的大致結構是這樣:
function createStore(reducer, preloadedState, enhancer) { if(enhancer是有效的){//這個我們後面再解釋,現在可以先不管 return enhancer(createStore)(reducer, preloadedState) } let currentReducer = reducer // 當前store中的reducer let currentState = preloadedState // 當前store中儲存的狀態 let currentListeners = [] // 當前store中放置的監聽函式 let nextListeners = currentListeners //下一次dispatch時的監聽函式 //注意:當我們新新增一個監聽函式時,只會在下一次dispatch的時候生效。 //... // 獲取state function getState() { //... } // 新增一個監聽函式,每當dispatch被呼叫的時候都會執行這個監聽函式 function subscribe() { //... } // 觸發了一個action,因此我們呼叫reducer,得到的新的state,並且執行所有新增到store中的監聽函式。 function dispatch() { //... } //... //dispatch一個用於初始化的action,相當於呼叫一次reducer //然後將reducer中的子reducer的初始值也獲取到 //詳見下面reducer的實現。 return { dispatch, subscribe, getState, //下面兩個是主要面向庫開發者的方法,暫時先忽略 //replaceReducer, //observable } } 複製程式碼
可以看出,createStore方法建立了一個store,但是並沒有直接將這個store的狀態state返回,而是返回了一系列方法,外部可以通過這些些方法(getState)獲取state,或者間接地(通過呼叫dispatch)改變state。
至於state呢,被存在了閉包中。(不理解閉包的同學可以先去了解一下先)
我們再來詳細的看看每個模組是如何實現的(省略了錯誤處理的程式碼):
getState
function getState() { return currentState } 複製程式碼
簡單到髮指,其實這很像面向物件程式設計中封裝只讀屬性的方法,只提供資料的getter方法,而不直接提供setter。
subscribe
function subscribe(listener) { // 新增到監聽函式陣列 nextListeners.push(listener) let isSubscribe = true //設定一個標誌,標誌該監聽器已經訂閱了 // 返回取消訂閱的函式,即從陣列中刪除該監聽函式 return function unsubscribe() { if(!isSubscribe) { return // 如果已經取消訂閱過了,直接返回 } isSubscribe = false // 從下一輪的監聽函式陣列(用於下一次dispatch)中刪除這個監聽器。 const index = nextListeners.indexOf(listener) nextListeners.splice(index, 1) } } 複製程式碼
dispatch
function dispatch(action) { //呼叫reducer,得到新state currentState = currentReducer(currentState, action); //更新監聽陣列 nextListener = currentListener; //呼叫監聽陣列中的所有監聽函式 for(let i = 0; i < nextListener.length; i++) { const listener = nextListener[i]; listener(); } } 複製程式碼
createStore這個方法的基本功能我們已經實現了,但是呼叫createStore方法需要提供reducer,讓我們來思考一下reducer的作用。
combineReducers
在理解combineReducers之前,我們先來想想reducer的功能:reducer接受一箇舊的狀態和一個action,當這個action被觸發的時候,reducer處理後返回一個新狀態。
也就是說 ,reducer負責狀態的管理(或者說更新)。在實際使用中,我們應用的狀態是可以分成很多個模組的,比如一個典型社交網站的狀態可以分為:使用者個人資訊,好友列表,訊息列表等模組。理論上,我們可以手動用一個reducer去處理所有狀態的更新,但是這樣做的話,我們一個reducer函式的邏輯就會太多,容易產生混亂。
因此我們可以將處理邏輯(reducer)也按照模組劃分,每個模組再細分成各個子模組,這樣我們的邏輯就能很清晰的組合起來。
對於我們的這種需求,redux提供了combineReducers方法,可以把子reducer合併成一個總的reducer。
來看看redux原始碼中combineReducers的主要邏輯:
function combineReducers(reducers) { //先獲取傳入reducers物件的所有key const reducerKeys = Object.keys(reducers) const finalReducers = {} // 最後真正有效的reducer存在這裡 //下面從reducers中篩選出有效的reducer for(let i = 0; i < reducerKeys.length; i++){ const key= reducerKeys[i] if(typeof reducers[key] === 'function') { finalReducers[key] = reducers[key] } } const finalReducerKeys = Object.keys(finalReducers); //這裡assertReducerShape函式做的事情是: // 檢查finalReducer中的reducer接受一個初始action或一個未知的action時,是否依舊能夠返回有效的值。 let shapeAssertionError try { assertReducerShape(finalReducers) } catch (e) { shapeAssertionError = e } //返回合併後的reducer return function combination(state= {}, action){ //這裡的邏輯是: //取得每個子reducer對應得state,與action一起作為引數給每個子reducer執行。 let hasChanged = false //標誌state是否有變化 let nextState = {} for(let i = 0; i < finalReducerKeys.length; i++) { //得到本次迴圈的子reducer const key = finalReducerKeys[i] const reducer = finalReducers[key] //得到該子reducer對應的舊狀態 const previousStateForKey = state[key] //呼叫子reducer得到新狀態 const nextStateForKey = reducer(previousStateForKey, action) //存到nextState中(總的狀態) nextState[key] = nextStateForKey //到這裡時有一個問題: //就是如果子reducer不能處理該action,那麼會返回previousStateForKey //也就是舊狀態,當所有狀態都沒改變時,我們直接返回之前的state就可以了。 hasChanged = hasChanged || previousStateForKey !== nextStateForKey } return hasChanged ? nextState : state } } 複製程式碼
為什麼需要中介軟體
在redux的設計思想中,reducer應該是一個純函式
維基百科關於純函式的定義:
在程式設計中,若一個函式符合以下要求,則它可能被認為是 純函式 :
- 此函式在相同的輸入值時,需產生相同的輸出。函式的輸出和輸入值以外的其他隱藏資訊或狀態無關,也和由I/O裝置產生的外部輸出無關。
- 該函式不能有語義上可觀察的函式副作用,諸如“觸發事件”,使輸出裝置輸出,或更改輸出值以外物件的內容等。
純函式的輸出可以不用和所有的輸入值有關,甚至可以和所有的輸入值都無關。但純函式的輸出不能和輸入值以外的任何資訊有關。純函式可以傳回多個輸出值,但上述的原則需針對所有輸出值都要成立。若引數是傳引用呼叫,若有對引數物件的更改,就會影響函式以外物件的內容,因此就不是純函式。
總結一下,純函式的重點在於:
- 相同的輸入產生相同的輸出(不能在內部使用
Math.random
,Date.now
這些方法影響輸出) - 輸出不能和輸入值以外的任何東西有關(不能呼叫API獲得其他資料)
- 函式內部不能影響函式外部的任何東西(不能傳入的引用變數),即不會突變
reducer為什麼要求使用純函式,文件裡也有提到,總結下來有這幾點:
-
state是根據reducer創建出來的,所以reducer是和state緊密相關的,對於state,我們有時候需要有一些需求(比如列印每一階段的state)。
-
純函式更易於除錯
- 比如我們除錯時希望action和對應的新舊state能夠被打印出來,如果新state是在舊state上修改的,即使用同一個引用,那麼就不能打印出新舊兩種狀態了。
- 如果函式的輸出具有隨機性,或者依賴外部的任何東西,都會讓我們除錯時很難定位問題。
-
如果不使用純函式,新舊狀態使用同一個引用,那麼在比較新舊狀態對應的兩個物件時,我們就不得不深比較了,深比較是非常浪費效能的。如果對於有可能發生修改的物件(即有一個action被reducer可以處理時),我們直接新建一個物件,兩個物件有不同的地址,因此淺比較就可以了。
至此,我們已經知道了,reducer是一個純函式,那麼如果我們在應用中確實需要處理一些副作用(比如非同步處理,呼叫API等操作),那麼該怎麼辦呢?這就是中介軟體解決的問題。下面我們就來講講redux中的中介軟體。
中介軟體處理副作用的機制
中介軟體在redux中位於什麼位置,我們可以通過這兩張圖來看一下。
先來看看不用中介軟體時的redux工作流程:

- dispatch一個action
- 這個action被reducer處理
- reducer根據action更新store(中的state)
而用了中介軟體之後的工作流程是這樣的:

- dispatch一個action
- 這個action先被中介軟體處理(比如在這裡傳送一個非同步請求)
- 中介軟體處理結束後,再發送一個action(有可能是原來的action,也可能是不同的action,視中間功能而不同)
- 中介軟體發出的action可能繼續被另一箇中間件處理,進行類似3的步驟。即中介軟體可以鏈式串聯。
- 最後一箇中間件處理完後,dispatch一個符合reducer處理標準的action
- 這個標準的action被reducer處理,
- reducer根據action更新store(中的state)
那麼中介軟體該如何融合到redux中呢?
在上面的流程中,2-4的步驟是關於中介軟體的,但凡我們想要新增一箇中間件,我們就需要寫一套2-4的邏輯。如果每個中介軟體我們手動串聯的話,就不夠靈活,增刪改以及調整順序,都需要修改中介軟體串聯的邏輯。
所以redux提供了一種解決方案,將中介軟體的串聯操作進行了封裝,經過封裝後,上面的步驟2-5就可以成為一個整體,如下圖:

我們只需要改造store自帶的dispatch方法,action發生後,先給中介軟體處理,最後再dispatch一個action交給reducer去改變狀態。
中介軟體在redux的實現
還記得redux 的 createStore()
方法的第三個引數 enhancer
嗎:
function createStore(reducer, preloadedState, enhancer) { if(enhancer是有效的){ return enhancer(createStore)(reducer, preloadedState) } //... } 複製程式碼
在這裡,我們可以看到,enhancer(可以叫做強化器)是一個函式,這個函式接受一個’常規createStore函式’作為引數,返回一個加強後的createStore函式。
這個加強的過程中做的事情,其實就是改造dispatch,新增上中介軟體。redux提供的 applyMiddleware()
方法返回的就是一個enhancer。
applyMiddleware,顧名思義,應用中介軟體,輸入為若干中介軟體,輸出為enhancer。下面就來看看這個方法的原始碼:
function applyMiddleware(...middlewares) { // 返回一個函式A,函式A的引數是一個createStore函式。 // 函式A的返回值是函式B,其實也就是一個加強後的createStore函式,大括號內的是函式B的函式體 return createStore => (...args) => { //用引數傳進來的createStore建立一個store const store= createStore(...args) //注意,我們在這裡需要改造的只是store的dispatch方法 let dispatch = () => {// 一個臨時的dispatch //作用是在dispatch改造完成前呼叫dispatch只會列印錯誤資訊 throw new Error(`一些錯誤資訊`) } //接下來我們準備將每個中介軟體與我們的state關聯起來(通過傳入getState方法),得到改造函式。 const middlewareAPI = { getState: store.getState, dispatch: (...args) => dispatch(...args) } //middlewares是一個函式陣列,這個函式的返回值是一個改造dispatch的函式 //呼叫一下每個中介軟體函式,得到所有的改造函式 const chain = middlewares.map(middleware => middleware(middlewareAPI)) //將這些改造函式compose(翻譯:構成,整理成)成一個函式 //用compose後的函式去改造store的dispatch dispatch = compose(...chain)(store.dispatch) // compose方法的作用是,例如這樣呼叫: // compose(func1,func2,func3) // 返回一個函式: (...args) => func1( func2( func3(...args) ) ) // 即func3改造後是一個新的dispatch,新的dispatch繼續給func2改造... //返回store,用改造後的dispatch方法替換store中的dispatch return { ...store, dispatch } } } 複製程式碼
總結一下,applyMiddleware的作用是:
- 從middleware中獲取改造函式
- 把所有改造函式compose成一個改造函式
- 改造dispatch方法
總結
至此,redux的核心原始碼已經講完了,最後不得不感嘆,redux寫的真的美,真tm的簡潔。
redux的核心功能還是建立一個store來管理state。通過reducer的層級劃分,可以得到一顆state樹,這棵樹如何與其他框架(如react)共同工作,我會再寫一篇《 react-redux
原始碼解讀》的部落格探究探究這個問題,敬請期待。