標籤:

Redux 源碼分析

Redux 源碼分析

2 人贊了文章

1. 前言

為使頁面組件和業務邏輯解耦,前端相繼湧現出 MVC(Model-View-Controller)、MVP(Model-View-Presenter)、 MVVM(Model-View-ViewModel) 模式。在雙向數據流的實現中,同一個 View 可能會觸發多個 Model 的更新,並間接引起另一個 View 的刷新,使得狀態變更的線索及影響變得錯綜複雜。redux 延續了 flux 架構,倡導單向數據流模式、不能通過訪問器屬性修改數據,這樣就便於追蹤狀態變化的線索。

圖 1,redux 數據流圖

在 redux 的設計中,state 為全局緩存的狀態(存儲於 Store 中),action 為狀態變更的標識,派發特定的 action 將引起指定的 state 變更。

不得不指出,首先,在視圖組件的實現上,多個 View 可能會復用相同的 state,因此,在一個 View 中派發的 action 可能會影響另一個 View 的狀態,這樣的話,狀態管理上仍會有錯綜的線索,並不具備清晰性。其次,redux 以狀態變更的動作為著眼點,通過 redux 組織業務邏輯,不如包含數據及其變更動作的 Model 直觀。

2. 源起

redux 最初需要聚焦於實現 state = fn(state, action) 函數,用於刷新緩存的狀態值。在這個函數中,state 可能包含多個狀態屬性;action 的職責有兩種,其一使用 type 屬性標識狀態值作何變更,其二攜帶的附屬信息將作為引導狀態值變更的數據源。對於多種狀態值變更,可採用分治的思想將其簡化,即 childState = fn(childState, action)。

let state = { todoList: [{ text: Eat food, completed: true }, { text: Exercise, completed: false }], visibilityFilter: SHOW_COMPLETED};function visibilityFilter(state = SHOW_ALL, action) { if (action.type === SET_VISIBILITY_FILTER) { return action.filter; } else { return state; }};function todos(state = [], action) { switch (action.type) { case ADD_TODO: return state.concat([{ text: action.text, completed: false }]); case TOGGLE_TODO: return state.map((todo, index) => action.index === index ? { text: todo.text, completed: !todo.completed } : todo ) default: return state; }};function todoApp(state = {}, action) { return { todos: todos(state.todos, action), visibilityFilter: visibilityFilter(state.visibilityFilter, action) };};const Add_Todo_Action = { type: ADD_TODO, text: Go to swimming pool };const Toggle_Todo_Action = { type: TOGGLE_TODO, index: 1 };const Set_Visibility_Filter_Action = { type: SET_VISIBILITY_FILTER, filter: SHOW_ALL };todoApp(state, Add_Todo_Action);

通過這份來自官網的代碼示例,我們既能瞧見源碼作者最初聚焦的視點,又能看見 redux 的一大原則 —— 使用純函數實現狀態更新。

在 redux 中,函數 state = fn(state, action) 被稱為 reducer。與 Array.prototype.reduce(arrReducer, result) 方法相同的是,在數組的原型方法中,result 的終值通過遞歸調用 arrReducer 獲得;redux 中的 state 更新也是通過逐個調用 reducer 函數實現。如果我們把採用策略模式分而治之的示例代碼轉變為採用職責鏈模式實現,即 childState = fn(childState, action) 替換為 state = fn(state, action) 函數,傳入 reducer 中的為全量 state 數據,多個 reducer 構成鏈式結構,當前一個執行完成後,再執行下一個,這樣就更接近於 Array.prototype.reduce 方法的執行機制,我們也就更能看出更新狀態的函數為什麼會被叫做 reducer 了。

圖 2,reducer 工作的兩種可能性

在 redux 源碼中,串聯多個 state 實際採用的是策略模式。與示例代碼不同的是,state 狀態會以業務模塊的組織劃分成多個狀態管理模塊,同一個狀態管理模塊內部又包含多個狀態值。對於前者,redux 提供了 combineReducers 方法,用於複合多個 reducer 函數。對於後者,需要使用者手動複合。

3. 實現

3.1. store

上一節提到了狀態轉換的機制,這是在 action 被派發以後所執行的動作。這一節我們將串聯整個鏈路,包含 action 怎樣被派發,狀態值如何作緩存,以及更新等。

針對根據 action 觸發 reducer 執行這一命題,我們自然地會想到使用發布-訂閱模式加以處理,將 action.type 視為訂閱的主題,action 中其餘屬性作為提供給訂閱者的額外信息。然而 redux 的宗旨是使狀態變化的線索變得清晰,易於追蹤和調試,發布-訂閱模式和這一宗旨相悖。因為在發布-訂閱模式中,同一主題可以有多個訂閱者,也就意味著同一個 action 可以觸發多個 reducer,線索就會變得錯綜。在 redux 的設計中,一個 action 只能觸發某個特定的reducer 執行。這樣,我們就解釋了為什麼在 redux 源碼中,針對獨立狀態集的多個子 reducer 可以被複合成一個單一的全局總 reducer(簡單的,可以通過 switch 語句實現),用於負責處理全局狀態的變更。當 action 被派發時,只需調用緩存的全局總 reducer,就可以實現全局狀態的更新。

如果我們把總 reducer 稱為 finalReducer,全局狀態稱為 globalState,派發 action 的過程其實只在於喚起 finalReducer 的執行。在 redux 源碼中,無論 finalReducer,還是 globalState,都在 store 中維護。store 的執行機制就如下圖所示:

圖 3,store 的執行機制

為了實現上述機制,store 將 finalReducer, globalState 實現為緩存數據,並提供 getState, dispatch, replaceReducer 方法。其中,store.getState 用於獲取全局緩存的狀態值,store.dispatch 用於派發 action,store.replaceReducer 用於替換 finalReducer。在 redux 源碼中,store 表現為 createStore 模塊,其提供 createStore(reducer, initialState) 函數,用於設置 finalReducer, globalState 的初始值。

從源碼中抽出這部分內容,即為如下代碼(剔除參數校驗):

function createStore(reducer, preloadedState) { let currentReducer = reducer let currentState = preloadedState function getState() { return currentState } function dispatch(action) { if (typeof action.type === undefined) { throw new Error( Actions may not have an undefined "type" property. + Have you misspelled a constant? ) } currentState = currentReducer(currentState, action) return action } function replaceReducer(nextReducer) { currentReducer = nextReducer dispatch({ type: ActionTypes.REPLACE }) } dispatch({ type: ActionTypes.INIT }) return { dispatch, getState, replaceReducer }}

從上述代碼中,redux 在創建 store 的過程,會派發 action = { type: ActionTypes.INIT },意味著可以在應用初始化過程中更新 state;而 store.replaceReducer 方法的存在通常是為了支持編碼時的熱載入功能,同時又會派發 action = { type: ActionTypes.REPLACE }。從設計的角度考量源碼,這是無需多加關注的細節。

3.2. middleware

圖 3 中,我們也能看出,action 經由 dispatch 函數直接交給 finalReducer 函數,middleware 中間件的意義是在 action 傳入 dispatch 函數前,對 action 進行轉換等特殊處理,功能類似 sevelet 中對請求進行轉換、過濾等操作的 filter,或者 koa 中間件。redux 中間件的實現上也採用泛職責鏈模式,前一個中間件處理完成,交由下一個中間件進行處理。

圖 4,中間件轉換 action 流程

redux 只能從 dispatch 函數的參數中截取到 action,因此在固有程序插入中間件的機制是通過封裝 dispatch 函數來完成的,即函數 newDispatch = middleware(dispatch)。這樣,在newDispatch 函數體內,我們就能獲得使用者傳入的 action。

在多個中間件的串聯上,redux 藉助 Array.prototype.reduce 方法實現。redux 又將 getState, dispatch 作為參數傳入 middleware 中,作為工具函數。

使用 redux 時,編寫中間件採用 ({ getState, dispatch }) => dispatch => action => { } 形式。再次申明,參數 { getState, dispatch } 為 redux 中間件機制中傳入的輔助函數,參數 dispatch 為本次 action 派發過程中喚起執行的 store.dispatch 方法,其意義就是通過封裝該函數獲取它的參數 action,參數 action 就是本次實際被派發的 action,中間件實際需要處理的轉換對象。

圖 5,單個中間件的處理流程

function compose(...funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (...args) => a(b(...args)))}applyMiddleware(...middlewares) { return createStore => (...args) => { const store = createStore(...args) let dispatch = () => { throw new Error( `Dispatching while constructing your middleware is not allowed. ` + `Other middleware would not be applied to this dispatch.` ) } let chain = [] const middlewareAPI = { getState: store.getState, dispatch: (...args) => dispatch(...args) } chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } }}

通過上述代碼,我們也能看出,redux 植入中間件機制是通過 applyMiddleware 函數封裝 createStore 完成的。在重新構建的 createStore 函數體內,其實現也如上文指出的,就是逐個調用中間件函數,對 store.dispatch 方法進行封裝。值得借鑒的是,通過包裝函數增強原函數的功能,可以使新功能點無縫地插入到原代碼邏輯中。

回過頭再看 createStore 模塊,我們發現,redux 在 createStore 函數的實現上還有第三個參數 enhancer,其主要目的就是為 applyMiddleware 函數提供一個便捷的介面,enhancer 參數的意義也就在於包裝 createStore 函數。這又是一個細節,無需多加關注。

function createStore(reducer, preloadedState, enhancer) { if (typeof enhancer !== undefined) { if (typeof enhancer !== function) { throw new Error(Expected the enhancer to be a function.) } return enhancer(createStore)(reducer, preloadedState) }}

3.3. listener

以上內容無不與狀態更新環節相關聯,並沒有涉及 store 與視圖層 view 怎樣完成交互。針對這一命題,redux 採用了發布-訂閱模式。實際表現為,當 store 派發一個 action 時(可視為發出一個消息),都會促使監視器 listener 與觀察者 observer (可視為消息的接受者)運作其處理邏輯。

在具體實現過程中,監視器 listener 通過 store.subscribe 方法添加到 listeners 緩存隊列中;當 action 被派發時,其將被取出執行。對於觀察者 observer,首先通過 store.observable 方法獲得介面層面的可觀察對象,其次調用該可觀察對象的 subscribe 方法,將 observer.next 轉化為 listener,並添加到 listeners 緩存隊列中。這樣,當 action 被派發時,無論監視器 listener,還是觀察者 observer 的 next 方法都將得到執行。不同的是,listener 為無參執行,observer.next 將以即時的 globalState 作為參數。

圖 6,listener, observer 執行流程

function createStore(reducer, preloadedState, enhancer) { function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice() } } function subscribe(listener) { let isSubscribed = true ensureCanMutateNextListeners() nextListeners.push(listener) return function unsubscribe() { if (!isSubscribed) { return } if (isDispatching) { throw new Error( You may not unsubscribe from a store listener while the reducer is executing. + See http://redux.js.org/docs/api/Store.html#subscribe for more details. ) } isSubscribed = false ensureCanMutateNextListeners() const index = nextListeners.indexOf(listener) nextListeners.splice(index, 1) } } function observable() { const outerSubscribe = subscribe return { subscribe(observer) { if (typeof observer !== object) { throw new TypeError(Expected the observer to be an object.) } function observeState() { if (observer.next) { observer.next(getState()) } } observeState() const unsubscribe = outerSubscribe(observeState) return { unsubscribe } }, [$$observable]() { return this } } } function dispatch(action) { try { isDispatching = true currentState = currentReducer(currentState, action) } finally { isDispatching = false } const listeners = (currentListeners = nextListeners) for (let i = 0; i < listeners.length; i++) { const listener = listeners[i] listener() } return action } return { // ... subscribe, [$$observable]: observable }}

4. 工具函數

redux 提供了三個工具函數,分別是前文已給出的 compose 函數,以及 bindActionCreators, combineReducers 函數。compose 函數不再重複說明。

bindActionCreators 函數的意義在於支持動態配置 action。其實現原理是通過 actionCreator 函數生成 action,再調用 store.dispatch 方法加以派發。

function bindActionCreator(actionCreator, dispatch) { return function() { return dispatch(actionCreator.apply(this, arguments)) }}function bindActionCreators(actionCreators, dispatch) { if (typeof actionCreators === function) { return bindActionCreator(actionCreators, dispatch) } if (typeof actionCreators !== object || actionCreators === null) { throw new Error( `bindActionCreators expected an object or a function, instead received ${ actionCreators === null ? null : typeof actionCreators }. ` + `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?` ) } const keys = Object.keys(actionCreators) const boundActionCreators = {} for (let i = 0; i < keys.length; i++) { const key = keys[i] const actionCreator = actionCreators[key] if (typeof actionCreator === function) { boundActionCreators[key] = bindActionCreator(actionCreator, dispatch) } } return boundActionCreators}

combineReducers 函數的意義在於複合 reducer。其實現過程中校驗了初始狀態,狀態的 key 值是否有與之匹配的 reducer。

代碼段 6,combineReducers

function combineReducers(reducers) { const reducerKeys = Object.keys(reducers) const finalReducers = {} for (let i = 0; i < reducerKeys.length; i++) { const key = reducerKeys[i] if (process.env.NODE_ENV !== production) { if (typeof reducers[key] === undefined) { warning(`No reducer provided for key "${key}"`) } } if (typeof reducers[key] === function) { finalReducers[key] = reducers[key] } } const finalReducerKeys = Object.keys(finalReducers) let unexpectedKeyCache if (process.env.NODE_ENV !== production) { unexpectedKeyCache = {} } let shapeAssertionError try { // assertReducerShape 校驗 reducer 返回初始狀態非 undefined,且有兜底 state assertReducerShape(finalReducers) } catch (e) { shapeAssertionError = e } return function combination(state = {}, action) { if (shapeAssertionError) { throw shapeAssertionError } if (process.env.NODE_ENV !== production) { // getUnexpectedStateShapeWarningMessage 函數校驗 state 初始值和 reducer 各鍵的匹配程度 // state 初始值可通過 ActionTypes.INIT 設定或參數注入 const warningMessage = getUnexpectedStateShapeWarningMessage( state, finalReducers, action, unexpectedKeyCache ) if (warningMessage) { warning(warningMessage) } } let hasChanged = false const nextState = {} for (let i = 0; i < finalReducerKeys.length; i++) { const key = finalReducerKeys[i] const reducer = finalReducers[key] const previousStateForKey = state[key] const nextStateForKey = reducer(previousStateForKey, action) if (typeof nextStateForKey === undefined) { // reducer 返回值為 undefined 時,由 getUndefinedStateErrorMessage 函數拼接錯誤文案 const errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } nextState[key] = nextStateForKey hasChanged = hasChanged || nextStateForKey !== previousStateForKey } return hasChanged ? nextState : state }}

5. 其他

為簡化 redux 使用過程中的編碼量,可參考 dva 設計 redux-model 模塊,通過 namespace 複合 constants, actions, reducers 文件。

對於非同步請求的 loading 狀態,可通過製作 redux-loading-middleware 中間件模塊在全局層面作統一處理。

以上兩點,以及緩存在 store 中數據的設計(在狀態管理模塊確定之後,如何高效、穩妥地組織狀態數據通常是編寫業務代碼的重心),介於篇幅和能力的限制,我將不再作闡述。

6. 後記

對我這樣半道出家的人來說,閱讀源碼比如專研一本好書。先從薄處入手,藉由豐富的關聯性思想到每個可挖掘的點,視野漸漸變得開闊,紙張上的字句也會漸漸變得厚實。再從厚處著眼,借著內在已儲備的知識量,更容易撥開阻礙視線的枝蔓,洞見維繫著本質的主幹,作者構思的線條也會變得越來越簡明。像每一段求索經歷,這是一個從薄到厚、再從厚到薄的過程,就中的滋味不乏刑偵、推理的樂趣。但是,閱讀源碼譬如靠經驗增進技藝,對知識的汲取往往流於碎片化。對那些才能稍嫌拙劣、又想一探究竟的人來說,以閱讀源碼的方式攀升到系統化認知的高度,這當中所需的演繹過程將置那些流行、已成熟的技術體系於不顧,勢必會耗費莫大的心力,譬如繞一段未必能達到終點的遠路。我認為,科班生有一種高屋建瓴的視角,較之半道出家的人,他們具備更為全面的認知,更容易跳過沿途遭遇的細節,理出解決命題的主要線索。當然,假如有個人以一種談不上正確的方式探尋 api 或數學公式背後的奧秘,他的動機是值得鼓勵的。只是等他回落在簡單的哲學中,那就需要一段或長或短的時間了。

吳軍博士在《數學之美》中引用了牛頓的一句話,」(人們發覺)真理在形式上從來都是簡單的,而不是複雜和含混的。「在閱讀這本書的過程中,我既能感受到作者行文簡明扼要的美感,又能從作者的描述中體會到簡單哲學的分量。因為簡單,可以助人在錯綜的表象中洞悉本質,可以擺脫心理上的弊病,免於將學識敷在臉面上。我想,演繹得越多,仰賴於記憶的成分也越少,深藏在海平面下的設計礦產也越加豐富(在其上方形成的概念可以理解為變動不居的表象)。秉持著對簡單哲學的信奉,我開始寫作這篇分析 redux 源碼的文章。雖然以現有的編程功底,想要使這篇文章脈絡清晰、簡明,我仍然會有力所不逮的感覺。

總之,這篇文章是逐漸摸索的產物,其中難免錯謬與勉強,卻是我試圖在編程行業中登堂入室的中轉站。

7. 參考

[深入 React 技術棧 - 陳屹]

Redux 中文文檔

推薦閱讀:

圖解 Flux
[譯]構建應用狀態時,你應該避免不必要的複雜性

TAG:React | Redux | Flux |