Redux 原始碼深度解析(附帶視訊1月14號上傳)

yck發表於2018-01-08

本文是 Redux 原始碼解析視訊的文字版,如果你想看我一句句程式碼解析的視訊版,可以關注文末的公眾號。每週末都會發布一些視訊,每天也會推送一些我認為不錯的文章。

前言

同步更新在 我的Github

在進入正題前,我們首先來看一下在專案中是如何使用 Redux 的,根據使用步驟來講解原始碼。以 我開源的 React 專案 為例。

// 首先把多個 reducer 通過 combineReducers 組合在一起
const appReducer = combineReducers({
  user: UserReducer,
  goods: GoodsReducer,
  order: OrdersReducer,
  chat: ChatReducer
});
// 然後將 appReducer 傳入 createStore,並且通過 applyMiddleware 使用了中介軟體 thunkMiddleware
// replaceReducer 實現熱更新替換
// 然後在需要的地方發起 dispatch(action) 引起 state 改變
export default function configureStore() {
  const store = createStore(
    rootReducer,
    compose(
      applyMiddleware(thunkMiddleware),
      window.devToolsExtension ? window.devToolsExtension() : f => f
    )
  );

  if (module.hot) {
    module.hot.accept("../reducers", () => {
      const nextRootReducer = require("../reducers/index");
      store.replaceReducer(nextRootReducer);
    });
  }

  return store;
}
複製程式碼

介紹完了使用步驟,接下來進入正題。

原始碼解析

首先讓我們來看下 combineReducers 函式

// 傳入一個 object
export default function combineReducers(reducers) {
 // 獲取該 Object 的 key 值
  const reducerKeys = Object.keys(reducers)
  // 過濾後的 reducers
  const finalReducers = {}
  // 獲取每一個 key 對應的 value
  // 在開發環境下判斷值是否為 undefined
  // 然後將值型別是函式的值放入 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}"`)
      }
    }

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  // 拿到過濾後的 reducers 的 key 值
  const finalReducerKeys = Object.keys(finalReducers)
  
  // 在開發環境下判斷,儲存不期望 key 的快取用以下面做警告  
  let unexpectedKeyCache
  if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {}
  }
    
  let shapeAssertionError
  try {
  // 該函式解析在下面
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }
// combineReducers 函式返回一個函式,也就是合併後的 reducer 函式
// 該函式返回總的 state
// 並且你也可以發現這裡使用了閉包,函式裡面使用到了外面的一些屬性
  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)
      }
    }
    // state 是否改變
    let hasChanged = false
    // 改變後的 state
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
    // 拿到相應的 key
      const key = finalReducerKeys[i]
      // 獲得 key 對應的 reducer 函式
      const reducer = finalReducers[key]
      // state 樹下的 key 是與 finalReducers 下的 key 相同的
      // 所以你在 combineReducers 中傳入的引數的 key 即代表了 各個 reducer 也代表了各個 state
      const previousStateForKey = state[key]
      // 然後執行 reducer 函式獲得該 key 值對應的 state
      const nextStateForKey = reducer(previousStateForKey, action)
      // 判斷 state 的值,undefined 的話就報錯
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      // 然後將 value 塞進去
      nextState[key] = nextStateForKey
      // 如果 state 改變
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // state 只要改變過,就返回新的 state
    return hasChanged ? nextState : state
  }
}
複製程式碼

combineReducers 函式總的來說很簡單,總結來說就是接收一個物件,將引數過濾後返回一個函式。該函式裡有一個過濾引數後的物件 finalReducers,遍歷該物件,然後執行物件中的每一個 reducer 函式,最後將新的 state 返回。

接下來讓我們來看看 combinrReducers 中用到的兩個函式

// 這是執行的第一個用於拋錯的函式
function assertReducerShape(reducers) {
// 將 combineReducers 中的引數遍歷
  Object.keys(reducers).forEach(key => {
    const reducer = reducers[key]
    // 給他傳入一個 action
    const initialState = reducer(undefined, { type: ActionTypes.INIT })
    // 如果得到的 state 為 undefined 就拋錯
    if (typeof initialState === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined during initialization. ` +
          `If the state passed to the reducer is undefined, you must ` +
          `explicitly return the initial state. The initial state may ` +
          `not be undefined. If you don't want to set a value for this reducer, ` +
          `you can use null instead of undefined.`
      )
    }
    // 再過濾一次,考慮到萬一你在 reducer 中給 ActionTypes.INIT 返回了值
    // 傳入一個隨機的 action 判斷值是否為 undefined
    const type =
      '@@redux/PROBE_UNKNOWN_ACTION_' +
      Math.random()
        .toString(36)
        .substring(7)
        .split('')
        .join('.')
    if (typeof reducer(undefined, { type }) === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined when probed with a random type. ` +
          `Don't try to handle ${
            ActionTypes.INIT
          } or other actions in "redux/*" ` +
          `namespace. They are considered private. Instead, you must return the ` +
          `current state for any unknown actions, unless it is undefined, ` +
          `in which case you must return the initial state, regardless of the ` +
          `action type. The initial state may not be undefined, but can be null.`
      )
    }
  })
}

function getUnexpectedStateShapeWarningMessage(
  inputState,
  reducers,
  action,
  unexpectedKeyCache
) {
  // 這裡的 reducers 已經是 finalReducers
  const reducerKeys = Object.keys(reducers)
  const argumentName =
    action && action.type === ActionTypes.INIT
      ? 'preloadedState argument passed to createStore'
      : 'previous state received by the reducer'
  
  // 如果 finalReducers 為空
  if (reducerKeys.length === 0) {
    return (
      'Store does not have a valid reducer. Make sure the argument passed ' +
      'to combineReducers is an object whose values are reducers.'
    )
  }
    // 如果你傳入的 state 不是物件
  if (!isPlainObject(inputState)) {
    return (
      `The ${argumentName} has unexpected type of "` +
      {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
      `". Expected argument to be an object with the following ` +
      `keys: "${reducerKeys.join('", "')}"`
    )
  }
    // 將參入的 state 於 finalReducers 下的 key 做比較,過濾出多餘的 key
  const unexpectedKeys = Object.keys(inputState).filter(
    key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
  )

  unexpectedKeys.forEach(key => {
    unexpectedKeyCache[key] = true
  })

  if (action && action.type === ActionTypes.REPLACE) return

// 如果 unexpectedKeys 有值的話
  if (unexpectedKeys.length > 0) {
    return (
      `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` +
      `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` +
      `Expected to find one of the known reducer keys instead: ` +
      `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
    )
  }
}
複製程式碼

接下來讓我們先來看看 compose 函式

// 這個函式設計的很巧妙,通過傳入函式引用的方式讓我們完成多個函式的巢狀使用,術語叫做高階函式
// 通過使用 reduce 函式做到從右至左呼叫函式
// 對於上面專案中的例子
compose(
    applyMiddleware(thunkMiddleware),
    window.devToolsExtension ? window.devToolsExtension() : f => f
) 
// 經過 compose 函式變成了 applyMiddleware(thunkMiddleware)(window.devToolsExtension()())
// 所以在找不到 window.devToolsExtension 時你應該返回一個函式
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)))
}
複製程式碼

然後我們來解析 createStore 函式的部分程式碼

export default function createStore(reducer, preloadedState, enhancer) {
  // 一般 preloadedState 用的少,判斷型別,如果第二個引數是函式且沒有第三個引數,就調換位置
  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 函式
    return enhancer(createStore)(reducer, preloadedState)
  }
  // 判斷 reducer 是否是函式
  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }
  // 當前 reducer
  let currentReducer = reducer
  // 當前狀態
  let currentState = preloadedState
  // 當前監聽函式陣列
  let currentListeners = []
  // 這是一個很重要的設計,為的就是每次在遍歷監聽器的時候保證 currentListeners 陣列不變
  // 可以考慮下只存在 currentListeners 的情況,如果我在某個 subscribe 中再次執行 subscribe
  // 或者 unsubscribe,這樣會導致當前的 currentListeners 陣列大小發生改變,從而可能導致
  // 索引出錯
  let nextListeners = currentListeners
  // reducer 是否正在執行
  let isDispatching = false
  // 如果 currentListeners 和 nextListeners 相同,就賦值回去
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }
  // ......
}
複製程式碼

接下來先來介紹 applyMiddleware 函式

在這之前我需要先來介紹一下函式柯里化,柯里化是一種將使用多個引數的一個函式轉換成一系列使用一個引數的函式的技術。

function add(a,b) { return a + b }   
add(1, 2) => 3
// 對於以上函式如果使用柯里化可以這樣改造
function add(a) {
    return b => {
        return a + b
    }
}
add(1)(2) => 3
// 你可以這樣理解函式柯里化,通過閉包儲存了外部的一個變數,然後返回一個接收引數的函式,在該函式中使用了儲存的變數,然後再返回值。
複製程式碼
// 這個函式應該是整個原始碼中最難理解的一塊了
// 該函式返回一個柯里化的函式
// 所以呼叫這個函式應該這樣寫 applyMiddleware(...middlewares)(createStore)(...args)
export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
   // 這裡執行 createStore 函式,把 applyMiddleware 函式最後次呼叫的引數傳進來
    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.`
      )
    }
    let chain = []
    // 每個中介軟體都應該有這兩個函式
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    // 把 middlewares 中的每個中介軟體都傳入 middlewareAPI
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 和之前一樣,從右至左呼叫每個中介軟體,然後傳入 store.dispatch
    dispatch = compose(...chain)(store.dispatch)
    // 這裡只看這部分程式碼有點抽象,我這裡放入 redux-thunk 的程式碼來結合分析
    // createThunkMiddleware返回了3層函式,第一層函式接收 middlewareAPI 引數
    // 第二次函式接收 store.dispatch
    // 第三層函式接收 dispatch 中的引數
{function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
  // 判斷 dispatch 中的引數是否為函式
    if (typeof action === 'function') {
    // 是函式的話再把這些引數傳進去,直到 action 不為函式,執行 dispatch({tyep: 'XXX'})
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}
const thunk = createThunkMiddleware();

export default thunk;}
// 最後把經過中介軟體加強後的 dispatch 於剩餘 store 中的屬性返回,這樣你的 dispatch
    return {
      ...store,
      dispatch
    }
  }
}
複製程式碼

好了,我們現在將困難的部分都攻克了,來看一些簡單的程式碼

// 這個沒啥好說的,就是把當前的 state 返回,但是當正在執行 reducer 時不能執行該方法
function getState() {
    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 listener to be a function.')
    }
// 這部分最主要的設計 nextListeners 已經講過,其他基本沒什麼好說的
    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 http://redux.js.org/docs/api/Store.html#subscribe for more details.'
      )
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    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 http://redux.js.org/docs/api/Store.html#subscribe for more details.'
        )
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }
 
function dispatch(action) {
// 原生的 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?'
      )
    }
// 注意在 Reducers 中是不能執行 dispatch 函式的
// 因為你一旦在 reducer 函式中執行 dispatch,會引發死迴圈
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }
// 執行 combineReducers 組合後的函式
    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }
// 然後遍歷 currentListeners,執行陣列中儲存的函式
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }
 // 然後在 createStore 末尾會發起一個 action dispatch({ type: ActionTypes.INIT });
 // 用以初始化 state
複製程式碼

結尾

如果大家還對 Redux 有疑問的話可以在下方留言。該文章為視訊的文字版,視訊在 1月14號上傳。

大家可以關注我的公眾號,我在公眾號中會更新視訊,並且每晚都會推送一篇我認為不錯的文章。

Redux 原始碼深度解析(附帶視訊1月14號上傳)

相關文章