Redux原始碼淺析

是熊大啊發表於2019-02-11

Redux解決的問題

JavaScript 需要管理比任何時候都要多的 state (狀態)

state 在什麼時候,由於什麼原因,如何變化已然不受控制。

通過限制更新發生的時間和方式,Redux 試圖讓 state 的變化變得可預測。

Redux設計分析

三個原則

  • 單一資料來源
  • state是隻讀的 不可寫(想要修改就必須按照redux的單向資料流邏輯來操作),是實現單向資料流的根本保障
  • 使用純函式來執行修改 純函式意味著依賴單一,我們只需要派發一個用於描述state變化的action即可。 這讓時間旅行、記錄和熱更新成為可能 儘可能的簡化單向資料流,不需要魔法

流程圖

action => middleware => reducer(s) => Store

Redux原始碼淺析

功能設計

  • createStore(rootReducer, initStore, middleware). 建立store
  • applyMiddleware(…middlewares). 使用中介軟體 鏈式使用
  • compose(…fn). 巢狀函式
  • combineReducers(…reducer). 組合reducer
  • bindActionCreator(actionCreators, dispatch). 封裝多個action

這是redux提供的幾個關鍵檔案和它們的作用,其實簡單他們提供的功能不難發現他們函數語言程式設計的影子。redux裡面設計比較巧妙的點個人感覺是在中介軟體裡。middleware在redux中被設計為在action發起後,到達reducer之前的擴充點。我們可以利用middleware實現類似日誌記錄,錯誤定位或者路由,還有非同步處理action這些操作。

關鍵點分析

redux的原始碼是比較典型的FP風格,掌握一些基本FP概念,再去閱讀redux原始碼會輕鬆很多

  • 高階函式

Higher order functions can take functions as parameters and return functions as return values.

接受函式作為引數傳入,並能返回封裝後函式。

  • 科裡化

Currying > Currying is the technique of translating the evaluation of a function that takes multiple arguments into evaluating a sequence of functions, each with a single argument

是把接受多個引數的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,並且返回接受餘下的引數而且返回結果的新函式的技術。
add(1,2,3) => add(1)(2)(3)

  • Compose > Composes functions from right to left.
    組合函式,將從右向左執行。

    compose(subtract,multiple,add)(200)
    等同於 subtract(multiple(add(200)));

內部使用reduce,而不是直接巢狀。

單向資料流

store.dispatch(action) => middleware =>reducer => store

貼上實現單向資料流的關鍵原始碼(部分刪減)

dispatch函式的實現 (/createStore.js)

Redux原始碼淺析

每次我們呼叫原生dispatch時,都會有這樣的流程,在上圖第五行裡dispatch函式將拿到的action交給reducer函式處理,這裡的isDispatching變數用來控制在reducer函式執行過程中不允許再次dispatch,這個過程用try/finally提供可靠性;第十行取得當前監聽器函式列表的快照,在for迴圈中依次執行,這裡執行也是有講究,並沒有直接listener[i]()呼叫,而是採用了分割this的行為逐個呼叫監聽器函式。總結這個dispatch函式關鍵點如下

  • 狀態位控制流程,在reducer過程中不允許dispatch
  • 用快照的形式儲存監聽器列表,避免在監聽器函式中呼叫subscribe函式引發的不可預期行為
  • 隔離監聽器this,營造私有變數。

combineReducer函式的實現(/combineReducers.js)

Redux原始碼淺析
  • reducer name 決定了state節點的key 呼叫由store提供的dispatch函式,即可觸發reducer,將返回的state更新,並觸發state監聽器列表中的方法。

中介軟體到底做了什麼

中介軟體發揮作用的時間點在派發action後,達到reducer前,可以理解為在呼叫原生dispatch(action)前,使用了中介軟體。 與其按照時間節點來理解,倒不如說中介軟體是為了增強dispatch函式而做的設計
applyMiddleware的原始碼非常精煉

Redux原始碼淺析

帶著問題來閱讀原始碼,中介軟體是如何實現以下幾點功能的

  • 如何讓中介軟體都可以獲取到state
  • 如何讓中介軟體可鏈式使用
  • 中介軟體的函式簽名為什麼是middleware = store => next => action => { next(action) }
如何讓中介軟體都可以獲取到state

這裡宣告瞭一個middlewareAPI,通過裡面的getState方法就可以拿到store裡的資料,另外一個dispatch並沒有什麼實際的作用,就算呼叫了,它也會告訴你不能使用,這裡利用map將middlewareAPI傳入到每個中介軟體裡,構造了一個閉包,讓中介軟體可以訪問到state資料,這裡也利用了currying函式延遲執行的特性,它接受了引數執行但是返回的是另外一個待執行的函式。 如此就保證了每個中介軟體可以獲取到state,關鍵點在於中介軟體科裡化的設計,讓其可以延遲執行和引數複用。

const middlewareAPI = { getState: store.getState, dispatch: (...args) => dispatch(...args) } 
// 利用currying函式延遲執行的特性 
複製程式碼
如何讓中介軟體可鏈式使用

如何將中介軟體串聯起來,並儲存最後一個函式傳入的引數為store.dispatch 想實現這個特性就要用到compose組合函式, 將中介軟體串聯起來,並且最後一個函式入參為store.dispatch, 傳入的next就是下一個中介軟體,當然最後一個函式接受的next就是原生的store.dispatch,那個時候中介軟體就處理完畢,將action派發到reducer了。

const a = next => action => next(action) 
const b = next => action => next(action) 
const c = dispatch => action => dispatch(action) 
compose(c, b, a)(store.dispatch) 
// 原始碼實現 dispatch = compose(...chain)(store.dispatch) 
複製程式碼
中介軟體的函式簽名

函式簽名middleware = store => next => action => { next(action) }

其實看到這裡應該也大致明白了為什麼要這麼設計中介軟體,返回的第一個函式是為了保證中介軟體可以取到全域性狀態,返回的第二個函式是為了保證中介軟體可以依次呼叫。redux裡的中介軟體是一個科裡化的函式,其主要目的是為了利用其延遲計算和引數複用的能力,來實現中介軟體的眾多特性。

一些遺憾

redux雖然為我們解決了state的管理問題,但依然不是百分之百的完美。邏輯上redux提供了一套簡單可行且非常清晰規範的state管理方案,資料的單資料流和其三個原則,與之帶來的是會寫一些模板程式碼,如果使用了中介軟體,特別是redux-saga那種獨立規範特別多的中介軟體,會耗費我們很多的時間在寫模板上,雖然我們可以對資料流動掌控的特別精細,但是時間成本依然減緩了我們開發的效率。

改進方案

redux的改進應該在保留優勢設計,解決痛點的基礎上進行。其實在多數開發者使用redux時一般會對其做簡單的封裝再使用,對redux增加一些設計模式或是使用企業內部的diapatch增強方法,這裡拋磚引玉,提出幾個redux理想改進的幾個需要注意的地方

  • 儘可能保留redux的核心概念,降低學習成本
  • 減少redux模板程式碼,可以從提高複用性和提供內建模板的角度來減少開發者的重複工作
  • 能無縫接入redux的生態,支援中介軟體,enhancer
  • 保留redux的特性,保留其三個原則
  • 如何簡單抽象action和reducer之間的關係是一個非常重要的思考點
  • 提供對複雜場景的功能支援,比如動態增加reducer,提供多個store例項

相關文章