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

「Content」:本文章簡要分析 Redux & Redux 生態的原理及其實現方式。
「Require」:理解本文需要一定 redux 的使用經驗。
「Gain」:將收穫

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

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

  3. 學習函數式範式是如何在實踐中應用的大量優秀示例。

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

Redux 設計哲學

Single source of truth

只能存在一個唯一的全局數據源,狀態和視圖是一一對應關係

Data - View Mapping

State is read-only

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

Changes are made with pure functions

狀態更新通過一個純函數(Reducer)完成,它接受一個描述狀態如何變化的對象(Action)來生成全新的狀態。

 State Change

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

Single State + Pure Function

Redux 架構

Redux 組件

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

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執行了然後返回對應的store
return enhancer(createStore)(reducer,preloadedState)
}

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

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

  2. 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 做了兩件事 :

  1. 調用 reducer 產生新的 state。

  2. 調用訂閱的監聽函數。

/*
* 通過原型鏈判斷是否是普通對象
對於一個普通對象,它的原型是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的一個切片,之後對切片做修改,替換掉currentListeners
function 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<typeof dispatch>(...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 後的函數最終執行過程就變成了如下圖所示,高階函數數組返回的函數將是一個從左到右鏈式調用的過程。

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 如何獲取到全局狀態

  1. 通過 React Context 存儲全局狀態
export const ReactReduxContext =   /*#__PURE__*/ 
React.createContext<ReactReduxContextValue | null>(null)
  1. 把它封裝成 Provider 組件
function Provider({ store, context, children }: ProviderProps) {     
    const Context = context || ReactReduxContext     
    return <Context.Provider value={contextValue}>{children}</Context.Provider>  
}
  1. 提供獲取 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.

  1. subscribe
  const subscription = useMemo(
  () => createSubscription(store),     
  [store, contextSub]
  )   
  subscription.onStateChange = checkForUpdates
  1. 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()    
    }
  1. 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 (
<div style={{textAlign:'center', marginTop:'35%'}}>
    <h1 style={{color: 'green', fontSize: '500%'}}>{state.count}</h1>
    <button onClick={onDecrement} style={{marginRight: '10%'}}>decrement</button>
    <button onClick={onIncrement}>increment</button>         
</div>         
) 
}

小結

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

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

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

歡迎關注公衆號 前端 Sharing 收貨大廠一手好文章~

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/9gcKWiMuocGVnOZ6nshFxA