前端妹紙的進階之路——redux原始碼分析

段亦心發表於2018-06-04

第一次看原始碼,並沒有想象中的難哈,主要是redux的原始碼比較少,理解起來也比較簡單。看過之後,感覺更深入的理解了redux思想和函數語言程式設計的理念,建議大家可以去看一下嘻嘻,看完之後肯定會有收穫的。

我是對照著網上別人看過的原始碼筆記看的,寫這篇文章的原因呢,是想總結一下,因為我記性比較差啦,算是做一個筆記,方便以後複習。

redux的原始碼中,有6個js檔案,分別是:

  • index.js
  • createStore.js
  • combineReducers.js
  • bindActionCreators.js
  • compose.js
  • applyMiddleware.js

我們一個一個來分析吧~

index

這裡呢沒有太多需要講的,就是暴露了5個核心的api,分別是:

  • createStore:接收state和reducer,生成一顆狀態樹store
  • combineReducers:把子reducer合併成一個大reducer
  • bindActionCreators:把actionCreators和dispatch封裝成一個函式,也就是把他們兩繫結在一起
  • applyMiddleware:這是一箇中介軟體
  • compose:一個組合函式

createStore

首先,定義初始化的action

export const ActionTypes = {
  INIT: '@@redux/INIT'
}
複製程式碼

這個createStore函式,會傳入三個引數和一個返回值,分別是:

1、 @param {Function} reducer

這個reducer是一個函式,這個函式接收state和action,作一系列計算之後返回一個新的state。這裡就體現了函數語言程式設計的一些特性:

第一,這個reducer是一個純函式,純函式的特點是:對於相同的輸入,永遠會得到相同的輸出,而且沒有任何可觀察的副作用,也不依賴外部環境的狀態。不理解純函式的筒子們,可以上網搜尋一下。

第二,state是不可變的,我們這裡對state作的一些修改和計算,不是直接修改原來的資料,而是返回修改之後的資料,原來的資料是保持不變。這裡可以衍生到immutable,可以使用immutable和redux搭配使用。

2、@param {any} [preloadedState]

這是初始化的state,不多說。

3、@param {Function} [enhancer]

這個enhancer其實就是一箇中介軟體,它在redux3.1.0之後才加入的。相當於把store做一些增強處理,讓store更強大,功能更豐富,在之後的applyMiddleware那裡會詳細說的。這裡也體現了高階函式的思想,就像react-redux的connect方法一樣,做一些包裝處理之後,再返回。

4、@returns {Store}

這是返回的值,返回的是一棵狀態樹,也就是store啦。

這是做的原始碼分析,都寫在註釋裡了。createStore返回的最常用的三個api是dispatch,subscribe,getState,一般我們只要傳入reducer和preloadedState,就可以直接呼叫這三個方法,非常方便。

export default function createStore(reducer, preloadedState, enhancer) {
  //這裡是一些引數校驗
  //如果第二個引數為函式且沒有傳入第三個引數,那就交換第二個引數和第三個引數
  //意思是createSotre會認為你忽略了preloadedState,而傳入了一個enhancer
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    //如果傳入了第三個引數,但不是一個函式,就報錯
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    //這是一個高階函式呼叫方法。這裡的enhancer就是applyMiddleware(...middlewares)
    //enhancer接受createStore作為引數,對createStore的能力進行增強,並返回增強後的createStore
    //然後再將reducer和preloadedState作為引數傳給增強後的createStore,得到最終生成的store
    return enhancer(createStore)(reducer, preloadedState)
  }

  //reducer不是函式,報錯
  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }

  //宣告一些變數
  let currentReducer = reducer //當前的reducer函式
  let currentState = preloadedState//當前的狀態樹
  let currentListeners = [] // 當前的監聽器列表
  let nextListeners = currentListeners //更新後的監聽器列表
  let isDispatching = false //是否正在dispatch

  //判斷當前listener和更新後的listener是不是同一個引用,如果是的話對當前listener進行一個拷貝,防止在操作新的listener列表的時候對正在發生的業務邏輯造成影響
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  /**
   *
   * @returns {any} The current state tree of your application.
   */
  //返回當前的狀態樹
  function getState() {
    return currentState
  }

  /**
   *這個函式是給store新增監聽函式,把listener作為一個引數傳入,
   *註冊監聽這個函式之後,subscribe方法會返回一個unsubscribe()方法,來登出剛才新增的監聽函式
   * @param {Function} listener 傳入一個監聽器函式
   * @returns {Function} 
   */
  function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected listener to be a function.')
    }
    //註冊監聽
    let isSubscribed = true

    ensureCanMutateNextListeners()
    //將監聽器壓進nextListeners佇列中
    nextListeners.push(listener)

    //註冊監聽之後,要返回一個取消監聽的函式
    return function unsubscribe() {
      //如果已經取消監聽了,就返回
      if (!isSubscribed) {
        return
      }
      //取消監聽
      isSubscribed = false

      //在nextListeners中找到這個監聽器,並且刪除
      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

  /**
   * @param {Object} action 傳入一個action物件
   *
   * @returns {Object} 
   */
  function dispatch(action) {
    //校驗action是否為一個原生js物件
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
        'Use custom middleware for async actions.'
      )
    }

    //校驗action是否包含type物件
    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
        'Have you misspelled a constant?'
      )
    }

    //判斷是否正在派發,主要是避免派發死迴圈
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    //設定正在派發的標誌位,然後將當前的state和action傳給當前的reducer,用於生成新的state
    //這就是reducer的工作過程,純函式接受state和action,再返回一個新的state
    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    //得到新的state之後,遍歷當前的監聽列表,依次呼叫所有的監聽函式,通知狀態的變更
    //這裡沒有把最新的狀態作為引數傳給監聽函式,是因為可以直接呼叫store.getState()方法拿到最新的狀態
    const listeners = currentListeners = nextListeners
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    //返回action
    return action
  }

  /**
   *這個方法主要用於reducer的熱替換
   * @param {Function} nextReducer 
   * @returns {void}
   */
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }
    // 把傳入的nextReducer給當前的reducer
    currentReducer = nextReducer
    //dispatch一個初始action
    dispatch({ type: ActionTypes.INIT })
  }

  /**
   * 用於提供觀察者模式的操作,貌似是一個預留的方法,暫時沒看到有啥用
   * @returns {observable} A minimal observable of state changes.
   */
  function observable() {
    const outerSubscribe = subscribe
    return {
      /**
       * The minimal observable subscription method.
       * @param {Object} observer 
       * 觀察者應該有next方法
       * @returns {subscription} 
       */
      subscribe(observer) {
        //觀察者模式的鏈式結構,傳入當前的state
        if (typeof observer !== 'object') {
          throw new TypeError('Expected the observer to be an object.')
        }

        //獲取觀察者的狀態
        function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }

        observeState()
        //返回一個取消訂閱的方法
        const unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },

      [$$observable]() {
        return this
      }
    }
  }
  //初始化一個action
  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}
複製程式碼

combineReducers

combineReducers的作用是將之前切分的多個子reducer合併成一個大的reducer,也就是說將很多的小狀態樹合併到一棵樹上,整合成一棵完整的狀態樹。

這個函式接受一個引數,返回一個函式

1、@param {Object} reducers

這裡接收多個reducer,傳入的是一個物件

2、@returns {Function}

combineReducers的整個執行過程就是:將所有符合標準的reducer放進一個物件中,當dispatch一個action的時候,就遍歷每個reducer,來計算出每個reducer的state值。同時,每遍歷一個reducer,就判斷新舊state是否發生改變,來決定是返回新state還是舊state,這是做的一個優化處理。

原始碼分析如下,前面還有一部分是一些error資訊和warning資訊的處理,就沒有放進來了,感興趣的話可以自己去看一下完整的原始碼。

export default function combineReducers(reducers) {
  //獲取reducers的所有key值
  const reducerKeys = Object.keys(reducers)
  //最終生成的reducer物件
  const finalReducers = {}

  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}"`)
      }
    }
    //遍歷reducer,把key值都是function的reducer放進finalReducers物件中
    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  //得到finalReducers的key值陣列
  const finalReducerKeys = Object.keys(finalReducers)

  let unexpectedKeyCache
  if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {}
  }

  //檢測這些reducer是否符合標準
  let shapeAssertionError
  try {
    //檢測是否是redux規定的reducer形式
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  //計算state的邏輯部分
  return function combination(state = {}, action) {
    if (shapeAssertionError) {
      throw shapeAssertionError
    }

    //如果不是production(線上)環境,做一些警告
    if (process.env.NODE_ENV !== 'production') {
      const warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache)
      if (warningMessage) {
        warning(warningMessage)
      }
    }

    //標誌state是否改變
    let hasChanged = false
    //儲存新的state
    const nextState = {}

    for (let i = 0; i < finalReducerKeys.length; i++) {
      //遍歷finalReducerKeys的key值,也就是reducer的名字
      const key = finalReducerKeys[i]
      //得到reducer的vlaue值
      const reducer = finalReducers[key]
      //變化前的state值
      const previousStateForKey = state[key]
      //變化後的state值,把變化前的state和action傳進去,計算出新的state
      const nextStateForKey = reducer(previousStateForKey, action)
      //如果沒有返回新的reducer,就丟擲異常
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      //把變化後的state存入nextState陣列中
      nextState[key] = nextStateForKey
      //判斷state是否有改變
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    //如果改變了state就返回新的state,沒改變就返回原來的state
    return hasChanged ? nextState : state
  }
}
複製程式碼

bindActionCreators

bindActionCreators的作用是:將action與dispatch函式繫結,生成可以直接觸發action的函式。

//使用dispatch包裝actionCreator方法
function bindActionCreator(actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args))
}
/*
 * @param {Function|Object} actionCreators
 *
 * @param {Function} dispatch 
 *
 * @returns {Function|Object}
 * 
 */
export default function bindActionCreators(actionCreators, dispatch) {
  //actionCreators為函式,就直接呼叫bindActionCreator進行包裝
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error(
      `bindActionCreators expected an object or a function, instead received ${actionCreators === null ? 'null' : typeof actionCreators}. ` +
      `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
    )
  }

  //以下是actionCreators為物件時的操作
  //遍歷actionCreators物件的key值
  const keys = Object.keys(actionCreators)
  //儲存dispatch和actionCreator繫結之後的集合
  const boundActionCreators = {}
  //遍歷每一個物件,一一進行繫結
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}
複製程式碼

compose

compose叫做函式組合,是一個柯里化函式,將多個函式合併成一個函式,從右到左執行。這同時也是函數語言程式設計的特性。這個函式會在applyMiddleware中用到

/**
 * @param {...Function} funcs The functions to compose.
 * @returns {Function} A function obtained by composing the argument functions
 * from right to left. For example, compose(f, g, h) is identical to doing
 * (...args) => f(g(h(...args))).
 */

export default function compose(...funcs) {
  //判斷傳入引數的數量,做不同的處理
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }
  
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
複製程式碼

這一段程式碼的主要難點是在最後那一句,著重說一下reduce這個方法。這個reduce不是之前的reducer,這裡的reduce函式是es5的一個歸併陣列的方法,是從陣列的第一項開始,逐個遍歷陣列的所有項。

它接收兩個引數,一個是在每一項上呼叫的函式,還有一個可選引數,是作為歸併基礎的初始值。呼叫的那個函式又接收四個引數,前一個值,當前值,項的索引,和陣列物件。這個函式返回的任何值都會作為第一個引數自動傳遞給下一項。這樣說可能比較抽象,舉個例子:

[1,2,3,4,5].reduce((prev, cur) => {
    return prev + cur //輸出15
})
複製程式碼

用reduce就可以很快的求的陣列所有值相加的和。另外,還有一個reduceRight()方法,跟reduce是一樣的,只不過是從陣列的右邊開始遍歷的。

我們回到原始碼上面return funcs.reduce((a, b) => (...args) => a(b(...args))),這其實就是遍歷傳入的引數陣列(函式),將這些函式合併成一個函式,從右到左的執行。這就是中介軟體的創造過程,把store用一個函式包裝之後,又用另一個函式包裝,就形成了這種包菜式的函式。

applyMiddleware

applyMiddleware是用來擴充套件redux功能的,主要就是擴充套件store.dispatch的功能,像logger、redux-thunk就是一些中介軟體。

它的實現過程是:在dispatch的時候,會按照在applyMiddleware時傳入的中介軟體順序,依次執行。最後返回一個經過許多中介軟體包裝之後的store.dispatch方法。

如果理解了之前說的compose函式,這一段程式碼應該也很容易就能看懂啦。

/**
 * @param {...Function} middlewares 接收不定數量的中介軟體函式
 * @returns {Function} 返回一個經過中介軟體包裝之後的store
 */
export default function applyMiddleware(...middlewares) {
  //返回一個引數為createStore的匿名函式
  return (createStore) => (reducer, preloadedState, enhancer) => {
    //生成store
    const store = createStore(reducer, preloadedState, enhancer)
    //得到dispatch方法
    let dispatch = store.dispatch
    //定義中介軟體的chain
    let chain = []

    //在中介軟體中要用到的兩個方法
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    //把這兩個api給中介軟體包裝一次
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    //鏈式呼叫每一箇中介軟體,給dispatch進行封裝,再返回最後包裝之後的dispatch
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}
複製程式碼

總結

整個的原始碼就全部分析完了,我們可以看到,redux的原始碼很多地方都體現了函數語言程式設計的思想。函數語言程式設計寫出來的程式碼確實很漂亮很簡潔,但是理解起來也比較困難。這也只是函數語言程式設計的很小一部分,有興趣的話可以去了解一下其他的部分。

寫到這裡也差不多了,希望以後有機會能多看點原始碼,get一些新的知識,最後感謝宋老師的寶貴意見,bye

相關文章