Redux原始碼解讀

Juliiii發表於2018-05-01

前言

作為React全家桶的一份子,Redux為react提供了嚴謹周密的狀態管理。但Redux本身是有點難度的,雖然學習了React也有一段時間了,自我感覺算是入了門,也知道redux的大概流程。但其背後諸如creatstore,applymiddleware等API背後到底發生了什麼事情,我其實還是不怎麼了解的,因此最近花了幾天時間閱讀了Redux的原始碼,寫下文章紀錄一下自己看原始碼的一些理解。(redux4.0版本)

一、原始碼結構

Redux是出了名的短小精悍(恩,這個形容很貼切),只有2kb大小,且沒有任何依賴。它將所有的髒活累活都交給了中介軟體去處理,自己保持著很好的純潔性。再加上redux作者在redux的原始碼上,也附加了大量的註釋,因此redux的原始碼讀起來還是不算難的。

先來看看redux的原始碼結構,也就是src目錄下的程式碼:

原始碼結構
Redux原始碼結構圖.PNG

其中utils是工具函式,主要是作為輔助幾個核心API,因此不作討論。 (注:由於篇幅的問題,下面程式碼很多都刪除了官方註釋,和較長的warn)

二、具體組成

index.js是redux的入口函式具體程式碼如下:

2.1 index.js

import createStore from './createStore'
import combineReducers from './combineReducers'
import bindActionCreators from './bindActionCreators'
import applyMiddleware from './applyMiddleware'
import compose from './compose'
import warning from './utils/warning'
import __DO_NOT_USE__ActionTypes from './utils/actionTypes'

function isCrushed() {}
if (
  process.env.NODE_ENV !== 'production' &&
  typeof isCrushed.name === 'string' &&
  isCrushed.name !== 'isCrushed'
) {
  warning(
  )
}

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
  __DO_NOT_USE__ActionTypes
}
複製程式碼

其中isCrushed函式是用於驗證在非生產環境下 Redux 是否被壓縮,如果被壓縮就會給開發者一個 warn 的提示。

在最後index.js 會暴露 createStore, combineReducers, bindActionCreators, applyMiddleware, compose 這幾個redux最主要的API以供大家使用。

2.2 creatStore

createStore函式接受三個引數:

  • reducer:是一個函式,返回下一個狀態,接受兩個引數:當前狀態 和 觸發的 action;
  • preloadedState:初始狀態物件,可以很隨意指定,比如服務端渲染的初始狀態,但是如果使用 combineReducers 來生成 reducer,那必須保持狀態物件的 key 和 combineReducers 中的 key 相對應;
  • enhancer:是store 的增強器函式,可以指定為中介軟體,持久化 等,但是這個函式只能用 Redux 提供的 applyMiddleware 函式來進行生成

下面就是creactStore的原始碼,由於整體原始碼過長,且 subscribe 和 dispatch 函式也挺長的,所以就將 subscribe 和 dispatch 單獨提出來細講。

 import $$observable from 'symbol-observable'

import ActionTypes from './utils/actionTypes'
import isPlainObject from './utils/isPlainObject'

export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }
  // enhancer應該為一個函式
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    //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   // 當前的state,也就是getState返回的值
  let currentListeners = []  // 當前的訂閱store的監聽器
  let nextListeners = currentListeners // 下一次的訂閱
  let isDispatching = false // 是否處於 dispatch action 狀態中, 預設為false

  // 這個函式用於確保currentListeners 和 nextListeners 是不同的引用
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  // 返回state
  function getState() {
    if (isDispatching) {
      throw new Error(
        ......
      )
    }
    return currentState
  }

  // 新增訂閱
  function subscribe(listener) {
  ......
    }
  }
// 分發action
  function dispatch(action) {
    ......
  }

  //這個函式主要用於 reducer 的熱替換,用的少
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }
    // 替換reducer
    currentReducer = nextReducer
    // 重新進行初始化
    dispatch({ type: ActionTypes.REPLACE })
  }

  // 沒有研究,暫且放著,它是不直接暴露給開發者的,提供了給其他一些像觀察者模式庫的互動操作。
  function observable() {
    ......
  }

  // 建立一個store時的預設state
  // 用於填充初始的狀態樹
  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}

複製程式碼
subscribe
function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }

    if (isDispatching) {
      throw new Error(
        ......
      )
    }

    let isSubscribed = true
    // 如果 nextListeners 和 currentListeners 是一個引用,重新複製一個新的
    ensureCanMutateNextListeners()
    nextListeners.push(listener)

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

      if (isDispatching) {
        throw new Error(
          .......
        )
      }
      
      isSubscribed = false
      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      // 從nextListeners裡面刪除,會在下次dispatch生效
      nextListeners.splice(index, 1)
    }
  }
複製程式碼

有時候有些人會覺得store.subscribe用的很少,其實不然,是react-redux隱式的為我們幫我們完成了這方面的工作。subscribe函式可以給 store 的狀態新增訂閱監聽,一旦我們呼叫了 dispatch來分發action ,所有的監聽函式就會執行。而 nextListeners 就是儲存當前監聽函式的列表,當呼叫 subscribe,傳入一個函式作為引數時,就會給 nextListeners 列表 push 這個函式。同時呼叫 subscribe 函式會返回一個 unsubscribe 函式,用來解綁當前傳入的函式,同時在 subscribe 函式定義了一個 isSubscribed 標誌變數來判斷當前的訂閱是否已經被解綁,解綁的操作就是從 nextListeners 列表中刪除當前的監聽函式。

dispatch

dispatch是redux中一個非常核心的方法,也是我們在日常開發中最常用的方法之一。dispatch函式是用來觸發狀態改變的,他接受一個 action 物件作為引數,然後 reducer 就可以根據 action 的屬性以及當前 store 的狀態,來生成一個新的狀態,從而改變 store 的狀態;

function dispatch(action) {
    // action 必須是一個物件
    if (!isPlainObject(action)) {
      throw new Error(
        ......
      )
    }
    // type必須要有屬性,不能是undefined
    if (typeof action.type === 'undefined') {
      throw new Error(
        ......
      )
    }
    // 禁止在reducers中進行dispatch,因為這樣做可能導致分發死迴圈,同時也增加了資料流動的複雜度
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
//       當前的狀態和 action 傳給當前的reducer,用於生成最新的 state
      currentState = currentReducer(currentState, action)
    } finally {  
      // 派發完畢
      isDispatching = false
    }
    // 將nextListeners交給listeners
    const listeners = (currentListeners = nextListeners)
    // 在得到新的狀態後,依次呼叫所有的監聽器,通知狀態的變更
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
    return action
  }
複製程式碼

其中 currentState = currentReducer(currentState, action);這裡的 currentReducer 是一個函式,他接受兩個引數:

  • 當前狀態
  • action

然後返回計算出來的新的狀態。

2.3 compose.js

compose 可以接受一組函式引數,從右到左來組合多個函式,然後返回一個組合函式。它的原始碼並不長,但設計的十分巧妙:


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)))
}
複製程式碼

compose函式的作用其實其原始碼的註釋裡講的很清楚了,比如下面這樣:

compose(funcA, funcB, funcC)
複製程式碼

其實它與這樣是等價的:

compose(funcA(funcB(funcC())))
複製程式碼

ompose 做的只是讓我們在寫深度巢狀的函式時,避免了程式碼的向右偏移。

2.4 applyMiddleware

applyMiddleware也是redux中非常重要的一個函式,設計的也非常巧妙,讓人歎為觀止。

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    // 利用傳入的createStore和reducer和建立一個store
    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)
    }
    // 讓每個 middleware 帶著 middlewareAPI 這個引數分別執行一遍
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
  }
}
複製程式碼

通過上面的程式碼,我們可以看出 applyMiddleware 是個三級柯里化的函式。它將陸續的獲得三個引數:第一個是 middlewares 陣列,第二個是 Redux 原生的 createStore,最後一個是 reducer,也就是上面的...args;

applyMiddleware 利用 createStore 和 reducer 建立了一個 store,然後 store 的 getState 方法和 dispatch 方法又分別被直接和間接地賦值給 middlewareAPI 變數。

其中這一句我感覺是最核心的:

dispatch = compose(...chain)(store.dispatch)
複製程式碼

我特意將compose與applyMiddleware放在一塊,就是為了解釋這段程式碼。因此上面那段核心程式碼中,本質上就是這樣的(假設...chain有三個函式):

dispatch = f1(f2(f3(store.dispatch))))
複製程式碼

2.5 combineReducers

combineReducers 這個輔助函式的作用就是,將一個由多個不同 reducer 函式作為 value 的 object合併成一個最終的 reducer 函式,然後我們就可以對這個 reducer 呼叫 createStore 方法了。這在createStore的原始碼的註釋中也有提到過。

並且合併後的 reducer 可以呼叫各個子 reducer,並把它們返回的結果合併成一個 state 物件。 由 combineReducers() 返回的 state 物件,會將傳入的每個 reducer 返回的 state 按其傳遞給 combineReducers() 時對應的 key 進行命名。

下面我們來看原始碼,下面的原始碼刪除了一些的檢查判斷,只保留最主要的原始碼:

export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  // 有效的 reducer 列表
  const finalReducers = {}
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]
  const finalReducerKeys = Object.keys(finalReducers)

// 返回最終生成的 reducer
  return function combination(state = {}, action) {
    let hasChanged = false
    //定義新的nextState
    const nextState = {}
    // 1,遍歷reducers物件中的有效key,
    // 2,執行該key對應的value函式,即子reducer函式,並得到對應的state物件
    // 3,將新的子state掛到新的nextState物件上,而key不變
    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)
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
     // 遍歷一遍看是否發生改變,發生改變了返回新的state,否則返回原先的state
    return hasChanged ? nextState : state
  }
}
複製程式碼

2.6 bindActionCreators

bindActionCreators可以把一個 value 為不同 action creator 的物件,轉成擁有同名 key 的物件。同時使用 dispatch 對每個 action creator 進行包裝,以便可以直接呼叫它們。 bindActionCreators函式並不常用(反正我還沒有怎麼用過),惟一會使用到 bindActionCreators 的場景就是我們需要把 action creator 往下傳到一個元件上,卻不想讓這個元件覺察到 Redux 的存在,並且不希望把 dispatch 或 Redux store 傳給它。

// 核心程式碼,並通過apply將this繫結起來
function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
} 
// 這個函式只是把actionCreators這個物件裡面包含的每一個actionCreator按照原來的key的方式全部都封裝了一遍,核心程式碼還是上面的
export default function bindActionCreators(actionCreators, dispatch) {
  // 如果actionCreators是一個函式,則說明只有一個actionCreator,就直接呼叫bindActionCreator
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }
  // 如果是actionCreator是物件或者null的話,就會報錯
  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error(
    ... ... 
  }
 // 遍歷物件,然後對每個遍歷項的 actionCreator 生成函式,將函式按照原來的 key 值放到一個物件中,最後返回這個物件
  const keys = Object.keys(actionCreators)
  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
}
複製程式碼

小節

看一遍redux,感覺設計十分巧秒,不愧是大佬的作品。這次看程式碼只是初看,往後隨著自己學習的不斷深入,還需多加研究,絕對還能得到更多的體會。

相關文章