造一個 redux 輪子

文章源碼:https://github.com/Haixiang6123/my-redux

參考輪子:https://www.npmjs.com/package/redux

前言吐槽

Redux 應該是很多前端新手的噩夢。還記得我剛接觸 Redux 的時候也是剛從 Vue 轉過來的時候,覺得Redux 概念非常多,想寫一個 Hello World 都難。

文檔也是很難看懂,並不是看不懂英文,而是看的時候總會想:TMD在說泥🐴呢。看得出文檔想手把手把新手教好,結果卻是適得而反,囉嗦的排版和系統性地闡述讓新手越來越蒙逼。文檔還有一步令人窒息的操作:把 redux、react-redux、redux-toolkit 三個庫放在一起來講。靠,你的標題叫 redux 文檔啊,就講 Redux 不就行了嘛?搞得新手總會覺得 Redux 就是像 Vuex 一樣爲 React 量身訂做的,其實並不是。

Redux 和 React 的關係

Redux 和 React 根本沒關係。

看 Redux 的官網開頭:"A Predictable State Container for JS Apps"。再看 Vuex 的官網開頭:"Vuex is a state management pattern + library for Vue.js applications"

請問哪裏出現了 "react" 這個單詞了?

兩者的定位本來就不一樣:Redux 僅僅是個事件中心(事件總線,隨便怎麼叫),就是 for JS Apps 的。而 Vuex 除了事件中心,也是 for Vue.js applications 的。

解決了什麼問題

爲了重新認識 Redux,我們先搞清楚 Redux 到底是個啥、解決了什麼問題。

簡單來說:

  • 創建一個事件中心,裏面存一些數據,叫 store
  • 向外提供讀、寫操作,叫 getStatedispatch,通過分發事件修改數據,叫 dispatch(action)
  • 添加監聽器,每次 dispatch 數據改了,就觸發監聽器,達到監聽數據變化的效果,叫 subscribe

Redux 本來就是一個超級簡單的庫,只是文檔不知不覺把它寫複雜了,搞得新手無從下手,口口相傳覺得 Redux 很難、很複雜。其實 Redux 一點都不難、簡單得一批。

不信?下面就帶大家一起寫一個完整的 Redux。

createStore

這個函數創建一個 Object,裏面存放數據,並提供讀和寫方法。實現如下:

function createStore(reduce, preloadedState, enhancer) {
  let currentState = preloadedState // 當前數據(狀態)
  let currentReducer = reducer // 計算新數據(狀態)
  let isDispatching = false // 是否在 dispatch

  // 獲取 state
  function getState() {
    if (isDispatching) {
      throw new Error('還在 dispatching 呢,獲取不了 state 啊')
    }
    return currentState
  }

  // 分發 action 的函數
  function dispatch(action) {
    if (isDispatching) {
      throw new Error('還在 dispatching 呢,dispatch 不了啊')
    }

    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    return action
  }

  return {
    getState,
    dispatch
  }
}

上面將數據存於 currentStategetState 返回當前數據。在 dispatch 裏使用 reducer 計算新的數據(狀態)從而修改 currentState

上面還用 isDispatching 防止多重 dispatch 情況下操作同一資源的問題。

假如別人不給你傳 preloadedState,那 currentState 初始時就會爲 undefuned 了呀,undefined 作爲 state 是不行的。爲了解決這個問題,可以在 createStore 的時候直接 dispatch 一個 action,這個 action 不命中所有 reducer 裏的 case,那麼 reducer 都返回初始值,以此達到初始化 state 的目的,這也是爲什麼在 reducer 裏的 switch-case 的 default 一定要返回 state 而不是啥都不處理。

// 生成隨機字符串,注意這裏的 toString(36) 的 36 是基數
const randomString = () => Math.random().toString(36).substring(7).split('').join('.')

const actionTypes = {
  INIT: `@@redux/INIT${randomString()}`, // 爲了重名,追加隨機字符串
}

function createStore(reduce, preloadedState, enhancer) {
  ...

  // 獲取 state
  function getState() {
    ...
  }

  // 分發 action 的函數
  function dispatch(action) {
    ...
  }

  // 初始化
  dispatch({type: actionTypes.INIT})

  return {
    getState,
    dispatch
  }
}

然後就可以用我們的 Redux 啦~

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return state + action.payload
    case 'decrement':
      return state - action.payload
    default:
      return state
  }
}

const store = createStore(reducer, 1) // 1,不管有沒有初始值,都會 dispatch @@redux/INIT 來初始化 state

store.dispatch({ type: 'increment', payload: 2 }) // 1 + 2

console.log(store.getState()) // 3

isPlainObject 和 kindOf

Redux 對 action 是有要求的,一定要是普通對象。所以我們還要需要判斷一下,如果不是普通對象,就拋出錯誤並說明 action 此時的類型。

// 分發 action 的函數
function dispatch(action: A) {
  if (!isPlainObject(action)) { // 是不是純對象
    throw new Error(`不是純淨的 Object,是一個類似 ${kindOf(action)} 的東西`) // 不是,是一個類似 XXX 的東西
  }
  ...
}

這裏的 isPlainObjectkindOf 都是可以從 npm 裏的 is-plain-objectkind-of 獲得。這兩個包實現都很簡單。是不是會覺得:啊?就這?就這麼小的包都有幾萬的下載量???我自己實現也行啊。沒錯,前端開發就是這麼無聊,寫這麼小的包都能一炮而紅,只難當年還不會 JS 沒能奪得先機 😢。

這裏我們用 npm 包,自己實現一波吧:

首先是 isPlainObject,一般來說通過判斷 typeof obj === 'object' 就可以了,但是 typeof null 也是 object,這是因爲最初實現 JS 的時候,用 typevalue 表示 JS 的值,當 type === 0 時表示是 Object,而當初 null 的地址又爲 0x00 所以 null 的 type 一直是 0,因此 typeof null === null,可以 參考這裏。 另一個點是原型鍵只有一層。

const isPlainObject = (obj: any) => {
  // 檢查類型
  if (typeof obj !== 'object' || obj === null) return false

  // 檢查是否由 constructor 生成
  let proto = obj
  while (Object.getPrototypeOf(proto) !== null) {
    proto = Object.getPrototypeOf(proto)
  }

  return Object.getPrototypeOf(obj) === proto
}

export default isPlainObject

另一個函數 kindOf 實現就繁瑣多了,除了要判斷一些簡單的 typeof 值,還要判斷 Array, Date, Error 等多種對象。

const isDate = (value: any) => { // 是不是 Date
  if (value instanceof Date) return true
  return (
    typeof value.toDateString === 'function' &&
    typeof value.getDate === 'function' &&
    typeof value.setDate === 'function'
  )
}

const isError = (value: any) => { // 是不是 Error
  if (value instanceof Error) return true
  return (
    typeof value.message === 'string' &&
    value.constructor &&
    typeof value.constructor.stackTraceLimit === 'number'
  )
}

const getCtorName = (value: any): string | null => { // 獲取
  return typeof value.constructor === 'function' ? value.constructor.name : null
}

const kindOf = (value: any): string => {
  if (value === void 0) return 'undefined'
  if (value === null) return 'null'

  const type = typeof value
  switch (type) { // 有字面意思的值
    case 'boolean':
    case 'string':
    case 'number':
    case 'symbol':
    case 'function':
      return type
  }

  if (Array.isArray(value)) return 'array' //是不是數組
  if (isDate(value)) return 'date' // 是不是 Date
  if (isError(value)) return 'error' // 是不是 Error

  const ctorName = getCtorName(value)
  switch (ctorName) { // 構造函數中讀取類型
    case 'Symbol':
    case 'Promise':
    case 'WeakMap':
    case 'WeakSet':
    case 'Map':
    case 'Set':
      return ctorName
  }

  return type
}

上面兩個函數在學習 Redux 並不是很重要,不過可以我們提供實現這兩個工具函數的一些靈感,下次再次使用時我們也可以直接手寫出來。

replaceReducer

replaceReducer 這個函數別說用了,估計沒多少人聽說過。在 Code Spliting 的時候纔會用到。比如打包出來有 2 個 JS,第一個先加載了 reducer,第二個加載新的 reducer,這裏可以用 combineReducers 去完成合並。

const newRootReducer = combineReducers({
  existingSlice: existingSliceReducer,
  newSlice: newSliceReducer
})

store.replaceReducer(newRootReducer)

現在有太多做動態模塊、代碼分割的庫幫我們做了這些事情了,所以我們沒多大機會用到這個 API。

實現上也很簡單,就是把原來的 reducer 替換掉就可以了。

const actionTypes = {
  INIT: `@@redux/INIT${randomString()}`,
  REPLACE: `@@redux/REPLACE${randomString()}`
}

function createStore(reducer, preloadedState, enhancer) {
  ...
  function replaceReducer(nextReducer) {
    currentReducer = nextReducer

    dispatch({type: actionTypes.REPLACE} as A) // 重新初始化狀態

    return store
  }
  ...
}

上面除了直接替換,還 dispatch 了 @@redux/REPALCE 這個 action。把當前狀態都重置了。

subscribe

剛剛說到 Redux 需要監聽數據的變化,非常 Easy ~ 可以在 dispatch 的時候觸發所有監聽器。

function createStore(reducer, preloadedState, enhancer) {
  let currentState = preloadedState
  let currentReducer = reducer
  let currentListeners = [] // 當前監聽器
  let nextListeners = currentListeners // 臨時監聽器集合
  let isDispatching = false

  // 獲取 state
  function getState() {
    if (isDispatching) {
      throw new Error('還在 dispatching 呢,獲取不了 state 啊')
    }
    return currentState
  }

  // 分發 action 的函數
  function dispatch(action: A) {
    if (!isPlainObject(action)) {
      throw new Error(`不是純淨的 Object,是一個類似 ${kindOf(action)} 的東西`)
    }

    if (isDispatching) {
      throw new Error('還在 dispatching 呢,dispatch 不了啊')
    }

    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    const listeners = (currentListeners = nextListeners)
    listeners.forEach(listener => listener()) // 全部執行一次

    return action
  }

  // 將 nextListeners 作爲臨時 listeners 集合
  // 防止 dispatching 時出現的一些 bug
  function ensureCanMutateNextListeners() {
    if (nextListeners !== currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  // 訂閱
  function subscribe(listener: () => void) {
    if (isDispatching) {
      throw new Error('還在 dispatching 呢,subscribe 不了啊')
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener) // 添加監聽器

    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      if (isDispatching) {
        throw new Error('還在 dispatching 呢,unsubscribe 不了啊')
      }

      isSubscribed = false

      ensureCanMutateNextListeners()

      // 去掉當前監聽器
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
      currentListeners = null
    }
  }

  // 初始化
  dispatch({type: actionTypes.INIT})

  return {
    getState,
    dispatch,
    subscribe,
  }
}

上面有幾個點要注意:
currentListeners 用於執行監聽器,nextListeners 作爲臨時監聽器的存放數組用於增加和移除監聽器。弄兩個數組是爲了防止修改數組數組時出現一些奇奇怪怪的 Bug,和上面用 isDispatching 解決操作同一資源的問題是差不多的。

subscribe 的返回值爲 unsubscribe 函數,這一是種很常用的編碼設計:如果一個函數有 side-effect,那麼返回值最好就是取消 side-effect 的函數,例如 useEffect 裏的函數。

可能有人會問如果 subscribe 很多次,第一次的 unsubscribe 裏的 listener 還是第一次的 listener 麼?這是肯定的,因爲 listenerunsubscribe 構成了閉包,每次的 unsubscribe 一直會引用那一次的 listenerlistener 不會被銷燬。

使用的例子如下:

const store = createStore(reducer, 1)

const listener = () => console.log('hello')

const unsubscirbe = store.subscribe(listener)

// 1 + 2
store.dispatch({ type: 'increment', payload: 2 }) // 打印 "hello"

unsubscribe()

// 3 + 2
store.dispatch({ type: 'increment', payload: 2 }) // 不會打印 "hello"

observable

observable 是 tc39 提出的概念,表示一個可被觀察的東西,裏面也有一個 subscribe 函數,不同的是傳入的參數爲 Observer,這個 Observer 需要有一個 next 函數,將當前狀態生成下一個狀態。

剛剛已經實現 store 數據的監聽了,那 store 也可以看作爲一個可被觀察的東西。我們弄一個函數就叫 observable,返回內容即爲上面的 observable 的實現:

const $$observable = (() => (typeof Symbol === 'function' && Symbol.observable) || '@@observable')()

export default $$observable


function createStore<S, A extends Action>(reducer preloadedState, enhancer) {
  ...
  // 支持 observable/reactive 庫
  function observable() {
    const outerSubscribe = subscribe

    return {
      subscribe(observer: unknown) {
        function observeState() {
          const observerAsObserver = observer
          if (observerAsObserver.next) {
            observerAsObserver.next(getState())
          }
        }

        observeState() // 獲取當前 state
        const unsubscribe = outerSubscribe(observeState)
        return {unsubscribe}
      },
      [$$observable]() {
        return this
      }
    }
  }
  ...
}

可以像下面這樣去用:

const store = createStore(reducer, 1)

const next = (state) => state + 2 // 獲取下一個狀態的函數

const observable = store.observable()

observable.subscribe({next}) // 訂閱後 next 一下:1 + 2

store.dispatch({type: 'increment', payload: 2}) // 1 + 2 + 3

從上面可以看出,next 的效果就是一個累加的效果。一般人也用不到上面的特性,主要都是別的庫會用到,比如 redux-observable 這個輪子

applyMiddlewares

現在 createStore 已經完成差不多啦,還有第三個參數 enhancer 沒有用到。這個函數主要用於增強 createStore 的。在 createStore 裏直接傳入當前 createStore,enhance 之後返回一個船新的 createStore,再傳入原來的 reducerpreloadedState 生成 store:

function createStore<S, A extends Action>(reducer, preloadedState, enhancer) {
  if (enhancer) {
    return enhancer(createStore)(reducer, preloadedState)
  }
  ...
}

enhancer 函數有很多種實現方式,其中最常見,也是官方提供的就是 applyMiddlewares 這個增強函數。它的目的是通過多種中間件來增強 dispatch,而 dispatch 又是 store 裏的一員,相當於把 store 增強了,因此這個函數是個 enhancer。

在實現 applyMiddlewares 之前,我們要弄清楚中間件這個概念是怎麼來的呢?又是如何增強 dispatch 的呢?爲啥要用 applyMiddlewares 這個 enhancer 呢?

先從一個簡單的例子說起:假如現在我們想在每次 dispatch 後都要 console.log 一下,最簡單的方法:直接把 dispatch 改掉:

let originalDispatch = store.dispatch
store.dispatch = (action) => {  
    let result = originalDispatch(action)  
    console.log('next state', store.getState())  
    return result
}

需要注意的是 dispatch 是一個傳入 action 並返回 action 的函數,因此這裏要將 result 返回出去。

那假如我們再加個 Logger 2 呢?可能會是這樣:

const logger1 = (store) => {
    let originalDispatch = store.dispatch
    
    store.dispatch = (action) => {
        console.log('logger1 before')
        let result = originalDispatch(action) // 原來的 dispatch
        console.log('logger 1 after')
        return result
    }
}

const logger2 = (store) => {
    let originalDispatch = store.dispatch
    
    store.dispatch = (action) => {
        console.log('logger2 before')
        let result = originalDispatch(action) // logger 1 的返回函數
        console.log('logger2 after')
        return result
    }
}

logger1(store)
logger2(store)

// logger2 before -> logger1 before -> dispatch -> logger1 after -> logger2 after
store.dispatch(...)

上面的 logger1 和 logger 2 就叫做中間件,它們可以拿到上一次的 store.dispatch 函數,然後一頓操作生成新的 dispatch,再賦值到 store.dispatch 來增強 dispatch

值得注意的點是,雖然先執行 logger1 再執行 logger2,但是 dispatch 時會以

logger2 before -> logger1 before -> dispatch -> logger1 after -> logger2 after

“倒敘” 的方式來執行中間件的內容。

如果有更多的中間件,可以用數組存起來。初始化也不能像上面那樣跑腳本那樣初始化了,可以把初始化封裝爲一個函數,就叫 applyMiddlewares 吧:

function applyMiddleware(store, middlewares) {
    middlewares = middlewares.slice()   // 淺拷貝數組 
    middlewares.reverse() // 反轉數組

    // 循環替換dispatch   
    middlewares.forEach(middleware => store.dispatch = middleware(store))
}

剛剛提到如果正序初始化中間件,會出現“倒序”執行 dispatch 的情況,所以這裏要做中間件數組的反轉。而 reverse 會改變原數組,因此開頭要做一次數組的淺拷貝。

上面的寫法有一個問題:在 forEach 裏直接改變 store.dispatch 會產生 side-effect。遵循函數式的思路,我們應該生成好一個最終的 dispatch,再賦值到 store.dispatch 上。

怎麼生成最終 dispatch 呢?參考 dispatch 的傳入 action 返回 action 的思路,我們也可以弄一個傳入舊 dispatch 返回新 dispatch 的函數嘛。比如:

const dispatch1 = (dispatch) => {...}
const dispatch2 = (dispatch1) => {...}
const dispatch3 = (dispatch2) => {...}
...

但是這樣 store 就傳不進來了,不怕,合理運用柯里化可以完美解決我們的問題:

const logger1 => (store) => (next) => (action) => {
    console.log('logger1 before')
    let result = originalDispatch(action)
    console.log('logger 1 after')
    return result
}

const logger2 => (store) => (next) => (action) => {
    console.log('logger2 before')
    let result = originalDispatch(action)
    console.log('logger2 after')
    return result
}

function applyMiddleware(store, middlewares) {
    // 初始的 dispatch
    let dispatch = (action) => {
      throw new Error('還在構建 middlewares,不要 dispatch')
    }

    middlewares = middlewares.slice() // 淺拷貝數組 
    middlewares.reverse() // 反轉數組

    const middlewareAPI = {
      getState: store.getState,
      // 這裏先用初始的 dispatch,防止在構建過程中 dispatch 的情況
      // 如果直接用上面 dispatch 會有閉包的問題,構建的時候都會指向初始時的 dispatch,可能會出現一些奇奇怪怪的 Bug
      // 因此這裏用了新生成的函數
      dispatch: (...args) => dispatch(args)
    }

    // 怎麼生成最終的 dispatch 呢?
    const xxx = middlewares.map(middleware => middleware(middlewareAPI))
    ...
}

爲了像上面套娃般地生成新函數,需要用到 reduce 函數來將數組裏每個函數進行頭接尾尾接頭的操作,這樣的操作稱爲 compose

function compose(...funcs: Function[]) {
  if (funcs.length === 0) {
    return (arg) => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((prev, curt) => (...args: any) => prev(curt(...args)))
}

將中間件一個個傳入 compose(logger1, logger2) 時,就會出現:

logger1(
  logger1 before
  logger2(
    logger2 before
    dispatch -> 最原始的 dispatch
    logger2 after
  )
  logger2 after
)

的結構。這就是 Redux 最厲害的地方了,對中間件的處理十分的優雅,而且使用 reducer 還改變了函數的執行順序連上面的 reverse 都不需要了。

整理一下上面的改動,再把 applyMiddlewares 寫成 enhancer 的寫法:

function applyMiddlewares(...middlewares: Middleware[]) {
  return (createStore) => (reducer: Reducer, preloadState) => {
    const store = createStore(reducer, preloadState)

    let dispatch = (action) => {
      throw new Error('還在構建 middlewares,不要 dispatch')
    }

    const middlewareAPI: MiddlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(args)
    }

    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {...store, dispatch}
  }
}

到了這一步,你已經掌握了 Redux 的精髓中的精髓了。剩下的就是一些“雜魚”函數了。

combineReducers

一個非常無聊的函數,僅僅將一堆的 reducer 合併一個 reducer 而已。比如:

const nameReducer = () => '111'
const ageReducer = () => 222

const reducer = combineReducers({
  name: nameReducer,
  age: ageReducer
})

const store = createStore(reducer, {
  name: 'Jack',
  age: 18
})

store.dispatch({type: 'xxx'}) // state => {name: '111', age: 222}

怎麼合併呢?簡單得雅痞:

function combineReducers(reducers: ReducerMapObject) {
  return function combination(state, action: AnyAction) {
    let hasChanged = false
    let nextState = {}
    Object.entries(finalReducers).forEach(([key, reducer]) => {
      const previousStateForKey = state[key] // 以前的狀態
      const nextStateForKey = reducer(previousStateForKey, action) // 更新爲現在的狀態

      if (typeof nextStateForKey === 'undefined') {
        throw new Error('狀態不能是 undefined 啊')
      }

      nextState[key] = nextStateForKey // 設置最新狀態
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey // 改了沒有啊?
    })

    // reducer 的 key 的數目和 state 的 key 的數目是否一致
    hasChanged = hasChanged || Object.keys(finalReducers).length === Object.keys(state).length

    return hasChanged ? nextState : null
  }
}

本質上就是把 reducerMapObject 裏每個 reducer 都執行一遍,拿到新 state 更新對應 key 下的 state。當然,Redux 裏的對這個函數的實現也沒這麼簡單,它還做了很多異常情況的處理,如檢查 reducer 到底是不是合法的 reducer。那啥是合法的 reducer 啊?答:找不到狀態時不返回 undefined 就合法。

const randomString = () => Math.random().toString(36).substring(7).split('').join('.')

const actionTypes = {
  INIT: `@@redux/INIT${randomString()}`,
  REPLACE: `@@redux/REPLACE${randomString()}`,
  PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
}

function assertReducerShape(reducers: ReducerMapObject) {
  Object.values(reducers).forEach(reducer => {
    const initialState = reducer(undefined, {type: actionTypes.INIT})
    if (typeof initialState === 'undefined') {
      throw new Error('最開始 dispatch 後狀態不能爲 undefined')
    }

    const randomState = reducer(undefined, {type: actionTypes.PROBE_UNKNOWN_ACTION})
    if (typeof randomState === 'undefined') {
      throw new Error('亂 dispatch 後的狀態也不能是 undefined')
    }
  })
}

通過 dispatch @@redux/INIT@@redux/PROBE_UNKNOWN_ACTION 來判斷不命中 reducer 裏的 case 時有沒有返回 undefuned。當然還檢查了 state 啊、action 啊這些東西的合法性:

function getUnexpectedStateShapeWarningMessage(
  inputState: object,
  reducers: ReducerMapObject,
  action: Action,
  unexpectedKeyCache: {[key: string]: true}
) {
  if (Object.keys(reducers).length === 0) {
    return '都沒有 reducer 還 combine 個啥呀'
  }

  if (!isPlainObject(action)) {
    return '都說了 action 要是普通的 Object 了,還傳一些亂七八糟的東西進來??'
  }

  if (action.type === actionTypes.REPLACE) return // 因爲 replaceReducer,所以這個 reducer 作廢了

  // 收集 reducerMapObject 裏不存在的 key
  const unexpectedKeys = Object.keys(inputState).filter(
    key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
  )
  unexpectedKeys.forEach(unexpectedKey => unexpectedKeyCache[unexpectedKey] = true)

  if (unexpectedKeys.length > 0) {
    return `下面這些 Key 都不在 state 上:${unexpectedKeys.join(', ')}`
  }
}

這裏的 unexpectedKeyCache 是一個 Map,如果某個子 state 有錯,則設置爲 true,這個 Map 是爲了防止多次告警所做的緩存。

再次更新一下 combineReducers

function combineReducers(reducers: ReducerMapObject) {
  // 檢查是否爲函數
  let finalReducers: ReducerMapObject = {}
  Object.entries(reducers).forEach(([key, reducer]) => {
    if (typeof reducer === 'function') {
      finalReducers[key] = reducer
    }
  }, {})

  let shapeAssertionError: Error
  try {
    // 檢查 reducer 返回值是否有 undefined
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  // 用於收集狀態不存在的 key
  let unexpectedKeyCache: {[key: string]: true} = {}

  return function combination(state, action: AnyAction) {
    if (shapeAssertionError) throw shapeAssertionError

    const warningMessage = getUnexpectedStateShapeWarningMessage(
      state,
      finalReducers,
      action,
      unexpectedKeyCache
    )

    if (warningMessage) {
      console.log(warningMessage)
    }

    let hasChanged = false
    let nextState = {}
    Object.entries(finalReducers).forEach(([key, reducer]) => {
      const previousStateForKey = state[key]
      const nextStateForKey = reducer(previousStateForKey, action)

      if (typeof nextStateForKey === 'undefined') {
        throw new Error('狀態不能是 undefined 啊')
      }

      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    })

    // reducer 的 key 的數目和 state 的 key 的數目是否一致
    hasChanged = hasChanged || Object.keys(finalReducers).length === Object.keys(state).length

    return hasChanged ? nextState : null
  }
}

combineActionCreators

更無聊的一個函數:僅僅把多個 action creator 執行,返回一些 () => dispatch(actionCreator(xxx)) 的函數,比如:

const store = createStore(reducer, 1)

const combinedCreators = combineActionCreators({
  add: (offset: number) => ({type: 'increment', payload: offset}), // 加法 actionCreator
  minus: (offset: number) => ({type: 'decrement', payload: offset}), // 減法 actionCreator
}, store.dispatch)

combinedCreators.add(100)
combinedCreators.minus(2)

主要的“好處”是返回的 combinedCreators 裏直接 .add(100),這裏的 .add(100) 可以不用感知 dispatch 的存在。

具體實現如下:

// 綁定一個 actionCreator
function bindActionCreator(actionCreator, dispatch) {
  return function (this: any, ...args: any[]) {
    return dispatch(actionCreator.apply(this, args))
  }
}

// 綁定多個 actionCreator
const combineActionCreators = (actionCreators, dispatch) => {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  const boundActionCreators: ActionCreatorsMapObject = {}

  Object.entries(actionCreators).forEach(([key, actionCreator]) => {
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  })

  return boundActionCreators
}

代碼非常簡單,僅僅幫你執行一下 actionCreator,然後 dispatch 返回的 action。

官方希望的是你在某個地方(比如父組件 combineActionCreators 了),在另外的地方(比如子組件)就不需要拿到 dispatch 函數就可以直接 dispatch action。

理想很好,但是這個功能的前提是要有定義好 actionCreator,一般來說沒人會花時間定義 actionCreator,都是直接 dispatch。

總結

上面已經實現整個 redux 裏所有的 API 了,基本上是一模一樣的,沒有偷工減料。

當然,有一些細節,比如判斷參數是不是函數,是不是 undefined 是沒有做的。爲了不寫起來太長,比如影響閱讀體驗,TS 類型也是簡單定義,很多函數簽名的聲明也沒有弄。不過這些並不太重要,類型的判斷完全可以交給 TS 去做就好了,而 TS 的類型無需太多糾結,畢竟這不是 TS 教程嘛 😆

總結一下,我們都幹了什麼:

  • 實現一個事件總線 + 數據(狀態)中心
    • getState 獲取數據(狀態)
    • dispatch(action) 修改數據(狀態)
    • subscribe(listener) 添加修改數據時的監聽器,只要 dispatch 所有監聽器依次觸發
    • replaceReducer 用新 reducer 替換舊 reducer,一般人用不了,忘了吧
    • observable 爲了配合 tc39 搞的,準確地說是爲了配合 RxJS 搞的。一般人用不起,忘了吧
    • enhancer 傳入已有 createStore 一通亂搞後返回增強後的 createStore,最最最常見的 enhancer 爲 applyMiddlewares。一般人只會用 applyMiddlewares,記住這個就可以了
  • 實現 applyMiddlewares,將一堆中間件通過 compose 組合起來,執行過程爲“洋蔥圈”模型。其中中間件的作用是爲了增強 dispatch,在 dispatch 前後會做一些事情
  • 實現 compose,原理爲將一堆入參爲舊 dispatch,返回新 dispatch 的函數數組,使用 Array.reduce 組合,變成 mid1(mid2(mid3())) 無限套娃的形式
  • 實現 combineReducers,主要作用是將多個 reducer 組件成一個新 reducer,執行 dispatch 後,所有 map 裏的 reducer 都會被執行。當你用到了多個子狀態 Slice 時會用到,別的場景忘了吧
  • combineActionCreators,將多個 actionCreators 都執行一遍,並返回 () => dispatch(actionCreator()) 這樣的函數。這個直接忘了吧

看到這裏,是不是覺得 Redux 其實並沒有想象中那麼的複雜,所有的“難”,“複雜”只是自己給自己設置的,硬剛源碼才能戰勝恐懼 👊

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章