簡單梳理Redux的源碼與運行機制

歡迎訪問我的網站:https://www.neroht.com/react

前言

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

API概覽

redux源碼的index.js,看到了咱們最經常使用的幾個API:redux

  • createStore
  • combineReducers
  • bindActionCreators
  • applyMiddleware
  • compose

不着急分析,咱們先看一下Redux的基本用法:axios

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是怎麼工做的:segmentfault

  • 使用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建立的,那麼咱們來看一下它內部的邏輯api

createStore

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

  • reducer:一個純函數,接收上一個(或初始的)state,和action,根據action 的type返回新的state
  • preloadedState:一個初始化的state,能夠設置store中的默認值,
  • enhancer:加強器,用來擴展store的功能

暴露給咱們幾個經常使用的API:app

  • dispatch:接收一個action, 是一個object{type:'a_action_type'}做爲參數,以後其內部會調用reducer,根據這個action,和當前state,返回新的state。
  • subscribe:訂閱一個更新頁面的函數,放進linsteners數組,用於在reducer返回新的狀態的時候被調用,更新頁面。
  • getState:獲取store中的狀態

咱們先經過接收的參數和暴露出來的api梳理一下它的機制:dom

createStore接收上面提到的三個參數建立一個store,store是存儲應用全部狀態的地方。同時暴露出三個方法,UI能夠經過store.getState()獲取到store中的數據,
store.subscribe(),做用是讓store訂閱一個更新UI的函數,將這個函數push到listeners數組中,等待執行。
store.dispatch()是更新store中數據的惟一方法,dispatch被調用後,首先會調用reducer,根據當前的state和action返回新的狀態。而後循環調用listeners中的更新函數,
更新函數通常是咱們UI的渲染函數,函數內部會調用store.getState()來獲取數據,因此頁面會更新。async

看一下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
  }
}

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幾個比較核心的概念就講解完了,不得不說寫的真簡潔,函數之間的依賴關係讓我一度十分懵逼,要理解它仍是要用源碼來跑一遍例子,
一遍一遍地看。

總結一下redux就是建立一個store來管理全部狀態,觸發action來改變store。關於redux的使用場景是很是靈活的,能夠結合各類庫去用,我用慣了react,用的時候還要配合react-redux。

相關文章
相關標籤/搜索