一步一步分析Redux原始碼?

fanyang發表於2018-04-16

一步一步分析Redux原始碼

前言

最近做專案遇到了一些複雜資料處理,側面體會到一個良好的資料層設計對一個專案的穩定性和可維護性是多麼的重要。於是想以原始碼分析的方式總結一下當前的資料管理方式,首選redux。 我們可以通過Redux 的官方文件來了解其設計思想。 cn.redux.js.org/.

本文儲存在我的github上 歡迎fork or star github.com/yangfan0095…

Redux 原始碼入口 index.js

我們可以看到 Redux 對外匯出了以下5個模組。分別對應5個js檔案。

import createStore from './createStore'
import combineReducers from './combineReducers'
import bindActionCreators from './bindActionCreators'
import applyMiddleware from './applyMiddleware'
import compose from './compose'
...
export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose
}

複製程式碼
  • createStore 作用: 建立store

  • combineReducers 作用: 合併reducer

  • bindActionCreators 作用: 把一個 value 為不同 action creator 的物件,轉成擁有同名 key 的物件

  • applyMiddleware 作用:通過自定義中介軟體擴充Redux 介紹

  • compose 作用: 從右到左來組合多個函式

其中最主要的檔案就是 createStore 。

createStore

createStore該方法的主要作用是建立一個帶有初始化狀態樹的store。並且該狀態樹狀態只能夠通過 createStore提供的dispatch api 來觸發reducer修改。

原始碼如下:

import isPlainObject from 'lodash/isPlainObject'
import $$observable from 'symbol-observable'

export default function createStore(reducer, 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.')
    }

    return enhancer(createStore)(reducer, preloadedState)
  }

  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }
  ...
    
  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
  
複製程式碼

方法有三個引數 reducer ,preloadedState ,enhancer。其中reducer是生成store時通過combineReducers合成以後返回的一個方法combination 這個方法 輸入 state 和 action ,返回最新的狀態樹,用來更新state。 preloadedState 是初始化state資料 ,enhancer 是一個高階函式用於擴充store的功能 , 如redux 自帶的模組applyMiddleware就是一個enhancer函式。

看原始碼,首先js 函式傳遞的是形參。原始碼判斷第二個引數的型別,如果是function 那麼就說明傳入的引數不是initState. 所以就把第二個引數替換成enhancer 。這樣提高了我們的開發體驗。

 if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

複製程式碼

關於對於enhancer的操作,如果enhancer 存在 執行則下面這段語句。

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

    return enhancer(createStore)(reducer, preloadedState)
  }
  
複製程式碼

我在網上找了一個例子來了解。 首先enhancer 是一個類似於下面結構的函式。 函式返回一個匿名函式。由於js是傳值呼叫,所以這個enhancer 在 createStore 中執行的時候 已經變成了createStore => (reducer, initialState, enhancer) => { ... }) 然後直接執行這個方法。

export default function autoLogger() {
  return createStore => (reducer, initialState, enhancer) => {
    const store = createStore(reducer, initialState, enhancer)
    function dispatch(action) {
      console.log(`dispatch an action: ${JSON.stringify(action)}`);
      const result = store.dispatch(action);
      const newState = store.getState();
      console.log(`current state: ${JSON.stringify(newState)}`);
      return result;
    }
    return {...store, dispatch}
  }
}

複製程式碼

這裡總結一下,通過這個enhancer我們可以改變dispatch 的行為。 其實這個例子是通過在enhancer 方法中定義新的dispatch覆蓋store中的dispatch來使使用者訪問到我們通過enhancer新定義的dispatch。除此之外還可以看applyMiddleware這個方法,這個我們後面再講。

接下來我們看到這裡,配置了一些變數 ,初始化時 將初始化資料preloadedState 賦值給currentState 。這些變數實際上是一個閉包,儲存一些全域性資料。

  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false
  ...
  
複製程式碼

subscribe 方法

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

    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

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

      isSubscribed = false

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }
複製程式碼

subscribe 方法實現的是一個訂閱監聽器,引數listener是一個回撥函式,在每次執行dispatch後會被呼叫,如下面程式碼:

function dispatch(action){
    for (let i = 0; i < listeners.length; i++) {
        const listener = listeners[i]
        listener()
    }
    ...
    
複製程式碼

訂閱函式返回一個解除訂閱函式unsubscribe,傳入的監聽函式listener首先會被放入當前訂閱者回撥函式列表nextListeners中 ,在action 觸發 dispatch 時,首先combination 及 我們的當前傳入的renducer會更新最新的狀態樹,然後listener被呼叫用於更新呼叫方的資料 在React 中 這個呼叫一般是用來更新當前元件state。React 中我們使用react-redux 來與redux做連線。想了解更多可以檢視原始碼 github.com/reactjs/rea…

原始碼做了高度封裝,這裡找了一個大致的例子來觀察react-redux 中高階元件 Connect 如何訂閱redux 以及更新元件狀態。

static contextTypes = {
    store: PropTypes.object
  }

  constructor () {
    super()
    this.state = { themeColor: '' }
  }
  
  // 元件掛載前新增訂閱
  componentWillMount () {
    this._updateThemeColor()
    store.subscribe(() => this._updateThemeColor())
  }

  _updateThemeColor () {
    const { store } = this.context
    const state = store.getState()
    this.setState({ themeColor: state.themeColor })
  }

複製程式碼

我們呼叫store.subscribe(() => this._updateThemeColor()) 來訂閱store () => this._updateThemeColor()這個傳入的回撥就是 listener。當我們dispatch時 我們執行listener實際 上是在執行this.setState({ themeColor: state.themeColor })從而更新當前元件的對應狀態樹的state。

訂閱器在每次dispatch之前會將listener 儲存到nextListeners 中,相當於是一份快照。如果當你正在執行listener函式時,如果此時又收到訂閱或者接觸訂閱指令 ,後者不會立即生效 ,而是在下一次呼叫dispatch 會使用最新的訂閱者列表即nextListeners。 當呼叫dispatch時 將最新的訂閱者快照nextListeners 賦給 currentListeners這裡有篇部落格文章專門討論了這個話題

    const listeners = currentListeners = nextListeners
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
    
複製程式碼

ensureCanMutateNextListeners 函式

關於ensureCanMutateNextListeners函式的作用,我看了很多資料,但是都沒有找到很好的解釋。大抵上的作用是某些場景可能會導重複的listener被新增,從而導致當前訂閱者列表中存在兩個相同的處理函式。ensureCanMutateNextListeners的作用是為了規避這種現象發生。

  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }
  
複製程式碼

當我們以如下方式追加訂閱 ,執行dispatch時就會造成重複訂閱。 具體的例子可以看這個連結: React Redux source function ensureCanMutateNextListeners?

const store = createStore(someReducer);

function doSubscribe() {
  store.subscribe(doSubscribe);
}
doSubscribe(); 

複製程式碼

dispatch方法

我們來看dispatch函式,dispatch 用來分發一個action,觸發reducer改變當前狀態樹然後執行listener 更新元件state。這裡是內部的dispatch 方法, 傳入的action 是樸素的action 物件。 包含一個state 和 type 屬性。 如果需要action 支援更多功能可以使用enhancer增強。如支援promise ,可以使用 redux-promise 它的本質是一箇中介軟體。 在其內部對action做處理,並最終傳遞一個樸素的action 物件到dispatch方法中。我們在下面介紹 applyMiddleware 中還會討論這個話題。

   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?'
      )
    }

    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    const listeners = currentListeners = nextListeners
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }
  
複製程式碼

首先 dispatch 通過第三方引入的方法判斷isPlainObject先判斷 action 型別是否合法 。然後將當前的state 和action 傳入 currentReducer 函式中 ,currentReducer 處理得到最新的state 賦值給currentState。然後觸發所有已更新的 listener 來更新state

 try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    const listeners = currentListeners = nextListeners
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
    
複製程式碼

replaceReducer 方法

replaceReducer 方法用於動態更新當前currentReducer 。 通過對外暴露replaceReducer API, 外部可以直接呼叫這個方法來替換當前currentReducer。然後執行dispatch({ type: ActionTypes.INIT }) 其實相當於一個初始化的createStore操作。dispatch({ type: ActionTypes.INIT })的作用是當store被建立時,一個初始化的action { type: ActionTypes.INIT } 被分發當前所有的reducer ,reducer返回它們的初始值,這樣就生成了一個初始化的狀態樹。

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

    currentReducer = nextReducer
    dispatch({ type: ActionTypes.INIT })
  }
  
複製程式碼

getState 方法

返回當前狀態樹

  function getState() {
    return currentState
  }
  
複製程式碼

observable方法

observable 是通過私有屬性被暴露出去的 ,只供內部使用。 該函式對外返回一個subscribe方法,該方法可以用來觀察最小單位的state 狀態的改變。 這個方法的引數 observer 是一個具有next (型別為Function) 屬性的物件。

如下原始碼所示: 函式首先將createStore 下的subscribe方法賦值給outerSubscribe,在起返回的方法中 首先定義函式observeState ,然後將其傳入outeSubscribe。實際上是封裝了一個linster 引用subscribe來做訂閱。當訊息被分發時,就出發了這個 linster ,然後next方法下呼叫observer.next(getState()) 就獲取到了當前的state

    function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }
    observeState()
    const unsubscribe = outerSubscribe(observeState)
        
複製程式碼

要獲取更多關於observable 的資訊可以檢視github.com/tc39/propos…

function observable() {
    const outerSubscribe = subscribe
    return {
      subscribe(observer) {
        if (typeof observer !== 'object') {
          thr[$$observable]: observableow 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
      }
    }
  }
  
  
複製程式碼

createStore 到這裡就講完了,它是redux裡面最核心的一個方法。裡面提供了完整的觀察訂閱方法API 給第三方。

bindActionCreators

export default function bindActionCreators(actionCreators, dispatch) {
  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"?`
    )
  }

  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
}

function bindActionCreator(actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args))
}

複製程式碼

我們可以重點來看 bindActionCreator方法,這個方法很簡單 直接傳入一個原始action 物件 ,和 dispatch方法。返回一個分發action 的方法(...args) => dispatch(actionCreator(...args))。 我們一個原始的action 物件如下

 export function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  }
}
複製程式碼

例如react 中我們獲取到了更新的狀態值 要手動通過dispatch 去執行對應的reducer (listener )函式來更新狀態數state

dispatch(addTodo)
複製程式碼

這裡相當於是直接將我們手動dispatch的方法封裝起來。action 實際變成了 (...args) => dispatch(actionCreator(...args)) 我們執行的時候 自動完成了dispatch操作。 bindActionCreators 方法對我們的 對應檔案匯出的action方法 進行遍歷 分別執行bindActionCreator 最後返回一個更新後的action集合boundActionCreators

combineReducers

作用是將多個reducer 合併成一個reducer ,返回一個combination 方法。 combination 也就是createStore操作傳入的reducer 。這個方法接受一個state 一個action返回一個最新的狀態數

例如我們在執行dispatch時會呼叫currentState = currentReducer(currentState, action) 這行程式碼來更新當前的狀態樹, 然後執行 訂閱者回撥函式listener 更新訂閱者 (如react 元件)的state 。

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]

    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 = {}
  }

// 呼叫 assertReducerShape  判斷單個reducer函式合法性
  let shapeAssertionError
  try {
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

// 返回一個combination方法 這個方法傳入state 和action  返回一個新的狀態樹
  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
    const nextState = {}
    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)
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
  }
}

複製程式碼

compose函式 & applyMiddleware 函式

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)))
}

複製程式碼

關鍵看著一行 funcs.reduce((a, b) => (...args) => a(b(...args)))compose 函式的作用是從右到左去組合中介軟體函式。 我們看一下常用的中介軟體範例 redux-thunk

redux-logger

let logger = ({ dispatch, getState }) => next => action => {
    console.log('next:之前state', getState())
    let result = next(action)
    console.log('next:之前state', getState())
    return result
}

let thunk = ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
        return action(dispatch, getState)
    }
    return next(action)
}
複製程式碼

中介軟體的格式是輸入dispatch, getState 輸出 next

let middleware = ({ dispatch, getState }) => next => action => {
    ...//操作
    return next(action)
}
複製程式碼

這裡的compose函式輸入一個包含多箇中介軟體函式的陣列,然後通過reduce 迭代成 middleware1(middleware2(middleware3(...args))) 這種形式。 這裡由於js 傳值呼叫 ,每個中介軟體在傳入第二個中介軟體時 實際上已經被執行過一次。 所以第二個中介軟體傳入的args 是第一個中介軟體return的 next方法。所以 compose返回的函式可以簡化為 function returnComposeFunc = middleware(next) ,現在我們來看applyMiddleware

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    const store = createStore(reducer, preloadedState, enhancer)
    let dispatch = store.dispatch
    let chain = []

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

複製程式碼

這裡的applyMiddleware 是一個enhancer 方法用來增強store的dispatch 功能,這裡用來合併中介軟體。首先利用閉包給每個中介軟體傳遞它們需要的getState和dispatch。

const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
chain = middlewares.map(middleware => middleware(middlewareAPI))
複製程式碼

然後將所有中介軟體push 到一個陣列chain中。 然後執行compose函式。執行compose函式compose(...chain)(store.dispatch) compose返回一個returnComposeFunc 我們傳入一個store.dispatch ,然後會返回一個 方法 。 而這個方法就是我們新的dispatch方法。

 action => {
    if (typeof action === 'function') {
        return action(dispatch, getState)
    }
    return next(action)
}
複製程式碼

看到這裡不得不說這裡的程式碼只有寥寥幾行 ,卻是如此值得推敲,寫得非常之精妙!

寫到這裡 redux的原始碼已經看完了,在react中需要我們手動新增訂閱,我們使用react-redux 來為每個元件新增訂閱。react-redux 裡面要講的也有很多,後面會寫一篇專門分析。

最後歡迎拍磚

參考資料

相關文章