Redux通關簡潔攻略——看這一篇就夠了

再寫Redux,清楚地知道自己在做什麼,每行程式碼會產生什麼影響。

理解storeEnhancer middleware的工作原理,根據需求可以自己創造。

學習函式式正規化是如何在實踐中應用的大量優秀示例。

「Correction」:如有寫錯的地方,歡迎評論反饋

Redux設計哲學

Single source of truth

只能存在一個唯一的全域性資料來源,狀態和檢視是一一對應關係

Redux通關簡潔攻略——看這一篇就夠了

Data - View Mapping

State is read-only

狀態是隻讀的,當我們需要變更它的時候,用一個新的來替換,而不是在直接在原資料上做更改。

Changes are made with pure functions

狀態更新透過一個純函式(Reducer)完成,它接受一個描述狀態如何變化的物件(Action)來生成全新的狀態。

Redux通關簡潔攻略——看這一篇就夠了

State Change

純函式的特點是函式輸出不依賴於任何外部變數,相同的輸入一定會產生相同的輸出,非常穩定。使用它來進行全域性狀態修改,使得全域性狀態可以被預測。當前的狀態決定於兩點:1。 初始狀態 2。 狀態存在期間傳遞的Action序列,只要記錄這兩個要素,就可以還原任何一個時間點的狀態,實現所謂的“時間旅行”(Redux DevTools)

Redux通關簡潔攻略——看這一篇就夠了

Single State + Pure Function

Redux架構

Redux元件

state: 全域性的狀態物件,唯一且不可變。

store: 呼叫createStore 函式生成的物件,裡面封入了定義在createStore內部用於操作全域性狀態的方法,使用者透過這些方法使用Redux。

action: 描述狀態如何修改的物件,固定擁有一個type屬性,透過store的dispatch方法提交。

reducer: 實際執行狀態修改的純函式,由使用者定義並傳入,接收來自dispatch的

action作為引數,計算返回全新的狀態,完成state的更新,然後執行訂閱的監聽函式。

storeEnhancer: createStore的高階函式封裝,用於加強store的能力,redux提供的applyMiddleware是官方實現的一個storeEnhancer。

middleware: dispatch的高階函式封裝,由applyMiddleware把原dispatch替換為包含middleware鏈式呼叫的實現。

Redux構成

Redux通關簡潔攻略——看這一篇就夠了

Redux API 實現

Redux Core

createStore

createStore 是一個大的閉包環境,裡面定義了store本身,以及store的各種api。環境內部有對如獲取state 、觸發dispatch 、改動監聽等副作用操作做檢測的標誌,因此reducer 被嚴格控制為純函式。

redux設計的所有核心思想都在這裡面實現,整個檔案只有三百多行,簡單但重要,下面簡要列出了這個閉包中實現的功能及原始碼解析,以加強理解。

如果有storeEnhancer,則應用storeEnhancer

if (typeof enhancer !== ‘undefined’) {// 型別檢測if (typeof enhancer !== ‘function’) {。。。}// enhancer接受一個storeCreator返回一個storeCreator// 在應用它的時候直接把它返回的storeCreatetor執行了然後返回對應的storereturn enhancer(createStore)(reducer,preloadedState)}

否則dispatch一個INIT的action,目的是讓reducer產生一個初始的state。注意這裡的INIT是Redux內部定義的隨機數,reducer無法對它有明確定義的處理,而且此時的state可能為undefined,故為了能夠順利完成初始化,編寫reducer時候我們需要遵循下面的兩點規則:

處理未定義type的action,直接返回入參的state。

createStore如沒有傳入初始的state,則reducer中必須提供預設值。

// When a store is created, an “INIT” action is dispatched so that every// reducer returns their initial state。 This effectively populates// the initial state tree。dispatch({ type: ActionTypes。INIT } as A)

最後把閉包內定義的方法裝入store物件並返回

const store = {dispatch,subscribe,getState,replaceReducer, // 不常用,故略過[$$observable]: observable // 不常用,故略過}return store;

下面是這些方法的實現方式

getState

規定不能在reducer裡呼叫getState,符合條件就返回當前狀態,很清晰,不再贅述。

function getState(){if (isDispatching) { 。。。}return currentState}

dispatch

內建的dispatch 只提供了普通物件Action 的支援,其餘像AsyncAction 的支援放到了middleware 中。dispatch做了兩件事 :

呼叫reducer 產生新的state。

呼叫訂閱的監聽函式。

/** 透過原型鏈判斷是否是普通物件對於一個普通物件,它的原型是Object*/function isPlainObject(obj){ if (typeof obj !== ‘object’ || obj === null) return false let proto = obj // proto出迴圈後就是Object while (Object。getPrototypeOf(proto) !== null) { proto = Object。getPrototypeOf(proto) } return Object。getPrototypeOf(obj) === proto}function dispatch(action: A) { // 判斷一下是否是普通物件 if (!isPlainObject(action)) { 。。。 } // redux要求action中需要有個type屬性 if (typeof action。type === ‘undefined’) { 。。。 } // reducer中不允許使用 if (isDispatching) { 。。。 } // 呼叫reducer產生新的state 然後替換掉當前的state 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}

subscribe

訂閱狀態更新,並返回取消訂閱的方法。實際上只要發生dispatch呼叫,就算reducer 不對state做任何改動,監聽函式也一樣會被觸發,所以為了減少渲染,各個UI bindings中會在自己註冊的listener中做 state diff來最佳化效能。注意listener 是允許副作用存在的。

// 把nextListeners做成currentListeners的一個切片,之後對切片做修改,替換掉currentListenersfunction ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners。slice() }}function subscribe(listener: () => void) { // 型別檢測 if(typeof listener !== ‘function’){ 。。。 } // reducer 中不允許訂閱 if (isDispatching) { 。。。 } let isSubscribed = true ensureCanMutateNextListeners() nextListeners。push(listener) return function unsubscribe() { // 防止重複取消訂閱 if (!isSubscribed) { return } // reducer中也不允許取消訂閱 if (isDispatching) { 。。。 } isSubscribed = false ensureCanMutateNextListeners() const index = nextListeners。indexOf(listener) nextListeners。splice(index, 1) currentListeners = null }}

applyMiddleware

applyMiddleware 是官方實現的一個storeEnhance,用於給redux提供外掛能力,支援各種不同的Action。

storeEnhancer

從函式簽名可以看出是createStore的高階函式封裝。

type StoreEnhancer = (next: StoreCreator) => StoreCreator;

CreateStore 入參中只接受一個storeEnhancer ,如果需要傳入多個,則用compose把他們組合起來,關於高階函式組合的執行方式下文中的Redux Utils - compose有說明,這對理解下面middleware 是如何鏈式呼叫的至關重要,故請先看那一部分。

middleware

type MiddlewareAPI = { dispatch: Dispatch, getState: () => State } type Middleware = (api: MiddlewareAPI) => (next: Dispatch) => Dispatch

最外層函式的作用是接收middlewareApi ,給middleware提供store 的部分api,它返回的函式參與compose,以實現middleware的鏈式呼叫。

export default function applyMiddleware(。。。middlewares) { return (createStore) =>{ // 初始化store,拿到dispatch const store = createStore(reducer, preloadedState) // 不允許在middlware中呼叫dispath let dispatch: Dispatch = () => { throw new Error( ‘Dispatching while constructing your middleware is not allowed。 ’ + ‘Other middleware would not be applied to this dispatch。’ ) } const middlewareAPI: MiddlewareAPI = { getState: store。getState, dispatch: (action, 。。。args) => dispatch(action, 。。。args) } // 把api注入middlware const chain = middlewares。map(middleware => middleware(middlewareAPI)) // 重點理解 // compose後傳入dispatch,生成一個新的經過層層包裝的dispath呼叫鏈 dispatch = compose(。。。chain)(store。dispatch) // 替換掉dispath,返回 return { 。。。store, dispatch } } }

再來看一個middleware加深理解:redux-thunk 詩 redux 支援asyncAction ,它經常被用於一些非同步的場景中。

// 最外層是一箇中間件的工廠函式,生成middleware,並向asyncAction中注入額外引數 function createThunkMiddleware(extraArgument) { return ({ dispatch, getState }) => (next) => (action) => { // 在中介軟體裡判斷action型別,如果是函式那麼直接執行,鏈式呼叫在這裡中斷 if (typeof action === ‘function’) { return action(dispatch, getState, extraArgument); } // 否則繼續 return next(action); };}

Redux Utils

compose

compose(組合)是函數語言程式設計正規化中經常用到的一種處理,它建立一個從右到左的資料流,右邊函式執行的結果作為引數傳入左邊。

compose是一個高階函式,接受n個函式引數,返回一個以上述資料流執行的函式。如果引數陣列也是高階函式,那麼它compose後的函式最終執行過程就變成了如下圖所示,高階函式陣列返回的函式將是一個從左到右鏈式呼叫的過程。

Redux通關簡潔攻略——看這一篇就夠了

export default function compose(。。。funcs) { if (funcs。length === 0) { return (arg) => arg } if (funcs。length === 1) { return funcs[0] } // 簡單直接的compose return funcs。reduce( (a, b) => (。。。args: any) => a(b(。。。args)) )}

combineReducers

它也是一種組合,但是是樹狀的組合。可以建立複雜的Reducer,如下圖 實現的方法也較為簡單,就是把map物件用函式包一層,返回一個mapedReducer,下面是一個簡化的實現。

function combineReducers(reducers){ const reducerKeys = Object。keys(reducers) const finalReducers = {} for (let i = 0; i < reducerKeys。length; i++) { const key = reducerKeys[i] finalReducers[key] = reducers[key] } const finalReducerKeys = Object。keys(finalReducers) // 組合後的reducer return function combination(state, action){ 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’) { 。。。 } nextState[key] = nextStateForKey hasChanged = hasChanged || nextStateForKey !== previousStateForKey } hasChanged = hasChanged || finalReducerKeys。length !== Object。keys(state)。length return hasChanged ? nextState : state } } }

bindActionCreators

用actionCreator建立一個Action,立即dispatch它

function bindActionCreator(actionCreator,dispatch) { return function (this, 。。。args) { return dispatch(actionCreator。apply(this, args)) }}

Redux UI bindings

React-redux

React-redux 是Redux官方實現的React UI bindings。它提供了兩種使用Redux的方式:HOC和Hooks,分別對應Class元件和函式式元件。我們選擇它的Hooks實現來分析,重點關注UI元件是如何獲取到全域性狀態的,以及當全域性狀態變更時如何通知UI更新。

UI如何獲取到全域性狀態

透過React Context儲存全域性狀態

export const ReactReduxContext = /*#__PURE__*/ React。createContext(null)

把它封裝成Provider元件

function Provider({ store, context, children }: ProviderProps) { const Context = context || ReactReduxContext return {children} }

提供獲取store的 hook: useStore

function useStore(){ const { store } = useReduxContext()! return store }

State變更時如何通知UI更新

react-redux提供了一個hook:useSelector,這個hook向redux subscribe了一個listener,當狀態變化時被觸發。它主要做下面幾件事情。

When an action is dispatched, useSelector() will do a reference comparison of the previous selector result value and the current result value。 If they are different, the component will be forced to re-render。 If they are the same, the component will not re-render。

subscribe

const subscription = useMemo( () => createSubscription(store), [store, contextSub] ) subscription。onStateChange = checkForUpdates

state diff

function checkForUpdates() { try { const newStoreState = store。getState() const newSelectedState = latestSelector。current!(newStoreState) if (equalityFn(newSelectedState, latestSelectedState。current)) { return } latestSelectedState。current = newSelectedState latestStoreState。current = newStoreState } catch (err) { // we ignore all errors here, since when the component // is re-rendered, the selectors are called again, and // will throw again, if neither props nor store state // changed latestSubscriptionCallbackError。current = err as Error } forceRender() }

re-render

const [, forceRender] = useReducer((s) => s + 1, 0) forceRender()

脫離UI bindings,如何使用redux

其實只要完成上面三個步驟就能使用,下面是一個示例:

const App = ()=>{ const state = store。getState(); const [, forceRender] = useReducer(c=>c+1, 0); // 訂閱更新,狀態變更重新整理元件 useEffect(()=>{ // 元件銷燬時取消訂閱 return store。subscribe(()=>{ forceRender(); }); },[]); const onIncrement = ()=> { store。dispatch({type: ‘increment’}); }; const onDecrement = ()=> { store。dispatch({type: ‘decrement’}); } return (

{state。count}

) }

小結

Redux核心部分單純實現了它“單一狀態”、“狀態不可變”、“純函式”的設定,非常小巧。對外暴露出storeEnhancer 與 middleware已在此概念上新增功能,豐富生態。redux的發展也證明這樣的設計思路使redux拓展性非常強。

其中關於高階函式的應用是我覺得非常值得借鑑的一個外掛體系構建方式,不是直接設定生命週期,而是直接給予核心函式一次高階封裝,然後內部依賴compose完成鏈式呼叫,這可以降低外部開發者的開發心智。

Redux想要解決的問題是複雜狀態與檢視的對映難題,但Redux本身卻沒有直接實現,它只做了狀態管理,然後把狀態更新的監聽能力暴露出去,剩下的狀態快取、狀態對比、更新檢視就拋給各大框架的UI-bindings,這既在保持了自身程式碼的單一性、穩定性、又能給真正在檢視層使用redux狀態管理的開發者提供“類響應式”的State-View開發體驗。