1. 程式人生 > 實用技巧 >簡單梳理Redux的原始碼與執行機制

簡單梳理Redux的原始碼與執行機制

前言

前幾天寫了一篇react另一個狀態管理工具Unstated的原始碼解析。
開啟了我的看原始碼之路。想一想用了好長時間的redux,但從沒有深究過原理,遇到報錯更是懵逼,所以就啃了一遍它的原始碼,寫了這篇文章,
分享我對於它的理解。

API概覽

看一下redux原始碼的index.js,看到了我們最常用的幾個API:

  • createStore
  • combineReducers
  • bindActionCreators
  • applyMiddleware
  • compose

不著急分析,我們先看一下Redux的基本用法:

import react from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
const
root = document.getElementById('root') // reducer 純函式 const reducer = (state = 0, action) => { switch (action.type) { case 'INCREMENT': return state + 1 case 'DECREMENT': return state - 1 default: return state } } // 建立一個store const store = createStore(reducer) const render = () => ReactDOM.render( <div
> <span>{store.getState()}</span> <button onClick=={() => store.dispatch({ type: 'INCREMENT' })}>INCREMENT</button> <button onClick=={() => store.dispatch({ type: 'DECREMENT' })}>DECREMENT</button> </div>, root ) render() // store訂閱一個更新函式
,待dispatch之後,執行這個更新函式,獲取新的值 store.subscribe(render)

這裡實現的是一個點選按鈕加減數字的效果,點選觸發的行為,與展示在頁面上的數字變化,都是通過redux進行的。我們通過這個例子來分析一下redux是怎麼工作的:

  • 使用reducer建立一個store,便於我們通過store來與redux溝通
  • 頁面上通過store.getState()拿到了當前的數字,初始值為0(在reducer中)
  • store.subscribe(render),訂閱更新頁面的函式,在reducer返回新的值時,呼叫。(實際subscribe會把函式推入listeners陣列,在之後迴圈呼叫)
  • 點選按鈕,告訴redux,我是要增加還是減少(呼叫dispatch,傳入action)
  • 呼叫dispatch之後,dispatch函式內部會呼叫我們定義的reducer,結合當前的state,和action,返回新的state
  • 返回新的state之後,呼叫subscribe訂閱的更新函式,更新頁面

目前為止,我們所有的操作都是通過store進行的,而store是通過createStore建立的,那麼我們來看一下它內部的邏輯

createStore

createStore總共接收三個引數:reducer,preloadedState,enhancer,

  • reducer:一個純函式,接收上一個(或初始的)state,和action,根據action 的type返回新的state
  • preloadedState:一個初始化的state,可以設定store中的預設值,
  • enhancer:增強器,用來擴充套件store的功能

暴露給我們幾個常用的API:

  • dispatch:接收一個action, 是一個object{type:'a_action_type'}作為引數,之後其內部會呼叫reducer,根據這個action,和當前state,返回新的state。
  • subscribe:訂閱一個更新頁面的函式,放進linsteners陣列,用於在reducer返回新的狀態的時候被呼叫,更新頁面。
  • getState:獲取store中的狀態

我們先通過接收的引數和暴露出來的api梳理一下它的機制:

首先是接收上面提到的三個引數建立一個store,store是儲存應用所有狀態的地方。同時暴露出三個方法,UI可以通過store.getState()獲取到store中的資料,
store.subscribe(),作用是讓store訂閱一個更新UI的函式,將這個函式push到listeners陣列中,等待執行。
store.dispatch()是更新store中資料的唯一方法,dispatch被呼叫後,首先會呼叫reducer,根據當前的state和action返回新的狀態。然後迴圈呼叫listeners中的更新函式,
更新函式一般是我們UI的渲染函式,函式內部會呼叫store.getState()來獲取資料,所以頁面會更新。

看一下createStore函式的結構

createStore(reducer, preloadedState, enhancer) {
  // 轉換引數
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  function getState() {
    // 返回當前的state, 可以呼叫store.getState()獲取到store中的資料,
    ...
  }

  function subscribe(listener) {
    // 訂閱一個更新函式(listener),實際上的訂閱操作就是把listener放入一個listeners陣列
    // 然後再取消訂閱,將更新函式從listeners陣列內刪除
    // 但是注意,這兩個操作都是在dispatch不執行時候進行的。因為dispatch執行時候會迴圈執行更新函式,要保證listeners陣列在這時候不能被改變
    ...
  }

  function dispatch(action) {
    // 接收action,呼叫reducer根據action和當前的state,返回一個新的state
    // 迴圈呼叫listeners陣列,執行更新函式,函式內部會通過store.getState()獲取state,此時的state為最新的state,完成頁面的更新
    ...
  }

  return {
    dispatch,
    subscribe,
    getState,
  }

}

結構就是這樣,但是是如何串聯起來的呢?下面來看一下完整的程式碼(刪除了一些)

createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    // 有了這一層判斷,我們就可以這樣傳:createStore(reducer, initialState, enhancer)
    // 或者這樣: createStore(reducer, enhancer),其中enhancer還會是enhancer。
    enhancer = preloadedState
    preloadedState = undefined
  }
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    // enhancer的作用是擴充套件store,所以傳入createStore來改造,
    // 再傳入reducer, preloadedState生成改造後的store,這一有一點遞迴呼叫的意思
    return enhancer(createStore)(reducer, preloadedState)
  }

  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }

  let currentReducer = reducer // 當前的reducer,還會有新的reducer
  let currentState = preloadedState // 當前的state
  let currentListeners = [] // 儲存更新函式的陣列
  let nextListeners = currentListeners // 下次dispatch將會觸發的更新函式陣列
  let isDispatching = false //類似一把鎖,如果正在dispatch action,那麼就做一些限制

  // 這個函式的作用是判斷nextListeners 和 currentListeners是否是同一個引用,是的話就拷貝一份,避免修改各自相互影響
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  function getState() {
    // 正在執行reducer的時候,是不能獲取state的,要等到reducer執行完,返回新的state才可以獲取
    if (isDispatching) {
      throw new Error(
        'You may not call store.getState() while the reducer is executing. ' +
          'The reducer has already received the state as an argument. ' +
          'Pass it down from the top reducer instead of reading it from the store.'
      )
    }

    return currentState
  }

  function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }
    // 由於dispatch函式會在reducer執行完畢後迴圈執行listeners陣列內訂閱的更新函式,所以要保證這個時候的listeners陣列
    // 不變,既不能新增(subscribe)更新函式也不能刪除(unsubscribe)更新函式
    if (isDispatching) {
      throw new Error(
        'You may not call store.subscribe() while the reducer is executing. ' +
          'If you would like to be notified after the store has been updated, subscribe from a ' +
          'component and invoke store.getState() in the callback to access the latest state. ' +
          'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
      )
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    // 將更新函式推入到listeners陣列,實現訂閱
    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 https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
        )
      }

      isSubscribed = false
      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      // 取消訂閱
      nextListeners.splice(index, 1)
    }
  }

  function dispatch(action) {
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }

    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }
    // 正在dispatch的話不能再次dispatch,也就是說不可以同時dispatch兩個action
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      // 獲取到當前的state
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    const listeners = (currentListeners = nextListeners)
    // 迴圈執行當前的linstener
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
    return action
  }

  // dispatch一個初始的action,作用是不命中你reducer中寫的任何關於action的判斷,直接返回初始的state
  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
    // observable  replaceReducer和$$observable主要面向庫開發者,這裡先不做解析
    // replaceReducer,
    // [$$observable]:
  }
}

combineReducers

combineReducers用於將多個reducer合併為一個總的reducer,所以可以猜出來,
它最終返回的一定是一個函式,並且形式就是一般的reducer的形式,接收state和action,
返回狀態:

function combine(state, action) {
  ......
  return state
}

來看一下核心程式碼:

export default function combineReducers(reducers) {
  // 獲取到所有reducer的名字,組成陣列
  const reducerKeys = Object.keys(reducers)

  // 這個finalReducers 是最終的有效的reducers
  const finalReducers = {}
  // 以reducer名為key,reducer處理函式為key,生成finalReducers物件,形式如下
  /* {
  *     reducerName1: f,
  *     reducerName2: f
  *  }
  */
  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

  // assertReducerShape用來檢查這每個reducer有沒有預設返回的state,
  // 我們在寫reducer時候,都是要在switch中加一個default的,來預設返回初始狀態
  try {
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  // 這個函式,就是上邊說的返回的最後的那個終極reducer,傳入createStore,
  // 然後在dispatch中呼叫,也就是currentReducer
  // 這個函式的核心是根據finalReducer中儲存的所有reducer資訊,迴圈,獲取到每個reducer對應的state,
  // 並依據當前dispatch的action,一起傳入當前迴圈到的reducer,生成新的state,最終,將所有新生成的
  // state作為值,各自的reducerName為鍵,生成最終的state,就是我們在reduxDevTool中看到的state樹,形式如下:
    /* {
    *     reducerName1: {
    *       key: 'value'
    *     },
    *     reducerName2: {
    *       key: 'value'
    *     },
    *  }
    */
  return function combination(state = {}, action) {
    if (shapeAssertionError) {
      throw shapeAssertionError
    }
    if (process.env.NODE_ENV !== 'production') {
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      )
      if (warningMessage) {
        warning(warningMessage)
      }
    }

    let hasChanged = false
    // 存放最終的所有的state
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
      // 獲取每個reducer的名字
      const key = finalReducerKeys[i]
      // 獲取每個reducer
      const reducer = finalReducers[key]
      // 獲取每個reducer的舊狀態
      const previousStateForKey = state[key]
      // 呼叫該reducer,根據這個reducer的舊狀態,和當前action來生成新的state
      const nextStateForKey = reducer(previousStateForKey, action)
      // 以各自的reducerName為鍵,新生成的state作為值,生成最終的state object,
      nextState[key] = nextStateForKey
      // 判斷所有的state變化沒變化
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // 變化了,返回新的state,否則,返回舊的state
    return hasChanged ? nextState : state
  }
}

資源搜尋網站大全 https://www.renrenfan.com.cn 廣州VI設計公司https://www.houdianzi.com

applyMiddleware

redux原本的dispatch方法只能接受一個物件作為action

使用者操作 -> dispatch(action) -> reducer(prevState, action) -> 新的state -> 介面

這麼直接乾脆的操作固然好,可以讓每一步的操作可追蹤,方便定位問題,但是帶來一個壞處,比如,頁面需要發請求獲取資料,並且把資料放到action裡面,
最終通過reducer的處理,放到store中。這時,如何做呢?

使用者操作 -> dispatch(action) -> middleware(action) -> 真正的action -> reducer(prevState, action) -> 新的state -> 介面

重點在於dispatch(action) -> middleware(action) 這個操作,這裡的action可以是一個函式,在函式內我們就可以進行很多操作,包括呼叫API,
然後在呼叫API成功後,再dispatch真正的action。想要這麼做,那就是需要擴充套件redux(改造dispatch方法),也就是使用增強器:enhancer:

const store = createStore(rootReducer,
  applyMiddleware(thunk),
)

applyMiddleware(thunk)就相當於一個enhancer,它負責擴充套件redux,說白了就是擴充套件store的dispatch方法。

既然要改造store,那麼就得把store作為引數傳遞進這個enhancer中,再吐出一個改造好的store。吐出來的這個store的dispatch方法,是enhancer改造store的最終實現目標。

回顧一下createStore中的這部分:

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

看下上邊的程式碼,首先判斷enhancer,也就是createStore的第三個引數不為undefined且為函式的時候,那麼去執行這個enhancer。

我們看到enhancer(createStore),是把createStore傳入,進行改造,先不管這個函式返回啥,我們先看它執行完之後還需要的引數
(reducer, preloadedState), 是不是有點眼熟呢?回想一下createStore的呼叫方法,createStore(reducer, state)。

由此可知enhancer(createStore)返回的是一個新的createStore,而這個createStore是被改造過後的,它內部的dispatch方法已經不是原來的了。至此,達到了改造store的效果。

那到底是如何改造的呢? 先不著急,我們不妨先看一個現成的中介軟體redux-thunk。要了解redux中介軟體的機制,必須要理解中介軟體是怎麼執行的。

我們先來看用不用它有什麼區別:

一般情況下,dispatch的action是一個純物件

store.dispatch({
    type:'EXPMALE_TYPE',
    payload: {
        name:'123',
    }
})

使用了thunk之後,action可以是函式的形式

function loadData() {
    return (dispatch, getState) => { // 函式之內會真正dispatch action
        callApi('/url').then(res => {
            dispatch({
                type:'LOAD_SUCCESS',
                data: res.data
            })
        })
    }
}

store.dispatch(loadData()) //派發一個函式

一般情況下,dispatch一個函式會直接報錯的,因為createStore中的dispatch方法內部判斷了action的型別。redux-thunk幫我們做的事就是改造dispatch,讓它可以dispatch一個函式。
看一下redux-thunk的核心程式碼:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
    return next(action);
  };
}
const thunk = createThunkMiddleware();

這裡的三個箭頭函式是函式的柯里化。

真正呼叫的時候,理論上是這樣thunk({ dispatch, getState })(next)(action)。

其中,thunk({ dispatch, getState})(next)這部分,看它執行時接收的引數是一個action,那麼它必然是一個dispatch方法,在此處相當於改造過後的dispatch,而這部分會在applyMiddleware中去呼叫,(下邊會講到)

然後從左往右看,{ dispatch, getState }是當前store的dispatch和getState方法,是最原始的,便於在經過中介軟體處理之後,可以拿到最原始的dispatch去派發真正的action。

next則是被當前中介軟體改造之前的dispatch。注意這個next,他與前邊的dispatch並不一樣,next是被thunk改造之前的dispatch,也就是說有可能是最原始的dispatch,也有可能是被其他中介軟體改造過的dispatch。

為了更好理解,還是翻譯成普通函式巢狀加註釋吧

function createThunkMiddleware(extraArgument) {
  return function({ dispatch, getState }) { //真正的中介軟體函式,內部的改造dispatch的函式是精髓
    return function(next) { //改造dispatch的函式,這裡的next是外部傳進來的dispatch,可能是被其他中介軟體處理過的,也可能是最原本的
      return function(action) { //這個函式就是改造過後的dispatch函式
        if (typeof action === 'function') {
          // 如果action是函式,那麼執行它,並且將store的dispatch和getState傳入,便於我們dispatch的函式內部邏輯執行完之後dispatch真正的action,
          // 如上邊示例的請求成功後,dispatch的部分
          return action(dispatch, getState, extraArgument);
        }
        // 否則說明是個普通的action,直接dispatch
        return next(action);
      }
    }
  }
}
const thunk = createThunkMiddleware();

總結一下:說白了,redux-thunk的作用就是判斷action是不是一個函式,是就去執行它,不是就用那個可能被別的中介軟體改造過的,也可能是最原始的dispatch(next)去派發這個action。

那麼接下來看一下applyMiddleware的原始碼:

export default function 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.'
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    const chain = middlewares.map(middleware => {
    // 假設我們只是用了redux-thunk,那麼此時的middleware就相當於thunk,可以往上看一下thunk返回的函式,
    // 就是這個: function({ dispatch, getState }),就會明白了
      return middleware(middlewareAPI)
    })
    // 這裡的compose函式的作用就是,將所有的中介軟體函式串聯起來,中介軟體1結束,作為引數傳入中介軟體2,被它處理,
    // 以此類推最終返回的是被所有中介軟體處理完的函式,最開始接收store.dispatch為引數,層層改造後被賦值到新的dispatch變數中
    dispatch = compose(...chain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
  }
}

先看最簡單的情況:假設我們只使用了一個middleware(redux-thunk),就可以暫時拋開compose,那麼這裡的邏輯就相當於
dispatch = thunk(middlewareAPI)(store.dispatch)
是不是有點熟悉? 在redux-thunk原始碼中我們分析過:

真正呼叫thunk的時候,thunk({ dispatch, getState })(next)(action)
其中,thunk({ dispatch, getState })(next)這部分,相當於改造過後的dispatch,而這部分會在applyMiddleware中去呼叫

所以,這裡就將store的dispatch方法改造完成了,最後用改造好的dispatch覆蓋原來store中的dispatch。

來總結一下,

  • 中介軟體和redux的applyMiddleware的關係。中介軟體(middleware)會幫我們改造原來store的dispatch方法
  • 而applyMiddleware會將改造好的dispatch方法應用到store上(相當於將原來的dispatch替換為改造好的dispatch)

理解中介軟體的原理是理解applyMiddleware機制的前提

另外說一下,關於redux-thunk的一個引數:extraArgument這個引數不是特別重要的,一般是傳入一個例項,然後在我們需要在真正dispatch的時候需要這個引數的時候可以獲取到,比如傳入一個axios 的Instance,那麼在請求時候就可以直接用這個instance去請求了

import axiosInstance from '../request'
const store = createStore(rootReducer, applyMiddleware(thunk.withExtraArgument(axiosInstance)))

function loadData() {
    return (dispatch, getState, instance) => {
        instance.get('/url').then(res => {
            dispatch({
                type:'LOAD_SUCCESS',
                data: res.data
            })
        })
    }
}

store.dispatch(loadData())

總結

到這裡,redux幾個比較核心的概念就講解完了,不得不說寫的真簡潔,函式之間的依賴關係讓我一度十分懵逼,要理解它還是要用原始碼來跑一遍例子,一遍一遍地看。