Redux解決的問題
JavaScript 需要管理比任何時候都要多的 state (狀態)
state 在什麼時候,由於什麼原因,如何變化已然不受控制。
通過限制更新發生的時間和方式,Redux 試圖讓 state 的變化變得可預測。
Redux設計分析
三個原則
- 單一資料來源
- state是隻讀的 不可寫(想要修改就必須按照redux的單向資料流邏輯來操作),是實現單向資料流的根本保障
- 使用純函式來執行修改 純函式意味著依賴單一,我們只需要派發一個用於描述state變化的action即可。 這讓時間旅行、記錄和熱更新成為可能 儘可能的簡化單向資料流,不需要魔法
流程圖
action => middleware => reducer(s) => Store
功能設計
- 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)
每次我們呼叫原生dispatch時,都會有這樣的流程,在上圖第五行裡dispatch函式將拿到的action交給reducer函式處理,這裡的isDispatching
變數用來控制在reducer函式執行過程中不允許再次dispatch,這個過程用try/finally提供可靠性;第十行取得當前監聽器函式列表的快照,在for迴圈中依次執行,這裡執行也是有講究,並沒有直接listener[i]()
呼叫,而是採用了分割this的行為逐個呼叫監聽器函式。總結這個dispatch函式關鍵點如下
- 狀態位控制流程,在reducer過程中不允許dispatch
- 用快照的形式儲存監聽器列表,避免在監聽器函式中呼叫subscribe函式引發的不可預期行為
- 隔離監聽器this,營造私有變數。
combineReducer函式的實現(/combineReducers.js)
- reducer name 決定了state節點的key 呼叫由store提供的dispatch函式,即可觸發reducer,將返回的state更新,並觸發state監聽器列表中的方法。
中介軟體到底做了什麼
中介軟體發揮作用的時間點在派發action後,達到reducer前,可以理解為在呼叫原生dispatch(action)前,使用了中介軟體。 與其按照時間節點來理解,倒不如說中介軟體是為了增強dispatch函式而做的設計
applyMiddleware的原始碼非常精煉
帶著問題來閱讀原始碼,中介軟體是如何實現以下幾點功能的
- 如何讓中介軟體都可以獲取到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例項