帶著問題看redux原始碼

瀟湘待雨發表於2019-05-21

前言

作為前端狀態管理器,這個比較跨時代的工具庫redux有很多實現和思想值得我們思考。在深入原始碼之前,我們可以相關注下一些常見問題,這樣帶著問題去看實現,也能更加清晰的瞭解。

常見問題

大概看了下主要有這麼幾個:

  1. redux三大原則
    這個可以直接參考官方文件
  2. redux 的優缺點。 關於優缺點,太主觀了大家見仁見智。
  3. redux中介軟體相關,洋蔥模型是什麼,常見中介軟體。

背景

有關acton,reducer相關的部分可以看我前面的文章。我們主要關注針對store和中介軟體相關的部分來解讀。

store的建立

作為維護和管理資料的部分,store在redux中的作用十分重要。在action發出指令,reduxer進行資料更新之後,監聽資料變化和同步資料更新的操作都要藉助store來實現。

createStore 輸入和輸出

首先看下createStore的使用,即常見的就是接受經過combineReducers處理之後的reducer和初始的state

import reducer from './reducers'
const store = createStore(reducer,initialState)

此外還可以接受第三個引數enhancer(增強器,一般就是applyMiddleware)


/**
 * 建立管理state 樹的Redux store
 * 應用中只能存在一個store,為了區分不同action對應的reducer,
 * 可以通過combineReducers來關聯不同的reducer
 * @param {Function} reducer   combineReducers關聯之後的reducer
 * @param {Object} preloadedState 初始state
 * @param {Function} enhancer 可以增強store功能的函式,例如中介軟體等。唯一適合
 * @returns 返回一個Store 以維護state和監聽變化  
 */
export default function createStore(reducer, preloadedState, enhancer) {
  // 如果第二個引數為func,redux認為忽略了初始state,而是
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    // enhancer增強劑,即利用中介軟體等來增強redux能力
    enhancer = preloadedState
    preloadedState = undefined
  }
  // 返回具有dispatch等屬性的物件 即store
  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
 } 

按照一般的執行順序,我們先看下對於引數的處理(平時大家也是一樣,一個函式,執行之前儘量判斷入參是否符合預期,避免直接處理造成的錯誤)

入參處理

對於三個引數,後兩個是非必填的,但如果第二個引數是function,reduxe認為其實encher,不然初始狀態是個函式不符合redux的預期,只能這樣處理了。

// 如果第二個引數為func,redux認為忽略了初始state,而是
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    // enhancer增強劑,即利用中介軟體等來增強redux能力
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    // 對於存在的enhancer,高階函式函式的用法,
    // 接受createStore返回一個增加功能之後的函式,然後再傳入相關reducer得到store。
    return enhancer(createStore)(reducer, preloadedState)
  }

  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }
  // 一切符合預期,沒有 enhancer,那麼初始賦值
  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  // 監聽佇列
  let nextListeners = currentListeners
  // dispatch標識
  let isDispatching = false
  
  // 初始狀態更新之後,宣告init狀態完成。
  dispatch({ type: ActionTypes.INIT })

dispatch的實現

dispatch的作用就是根據action,執行對應reducer以更新state。並執行監聽佇列。
下面就來看dispatch的用法和實現。
常見使用:

// redux要求 引數必須為純物件
dispatch({ type: ActionTypes.INIT })

那麼對於純物件,redux做了些什麼呢

 /**
   * 通知方法,引數為純的js物件,標明更新內容
   * @param {Object} action 
   */
  function dispatch(action) {
    // 是否滿足純淨物件
    if (!isPlainObject(action)) {
      throw new Error(
        '省略'
      )
    }
    // 必須的type是否存在 
    if (typeof action.type === 'undefined') {
      throw new Error(
        '省略'
      )
    }
    // 判斷是否處於某個action的dispatch中,大家一起dispatch可能死迴圈
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      // 開始dispatch,加鎖,標明狀態
      isDispatching = true
      // 將當前狀態和更新action,傳給當前reducer處理
      // 這裡可以回想下我們reducer中的兩個引數,state和action 對應的是什麼
      /**
       * const todos = (state = [], action) => {
       */
      currentState = currentReducer(currentState, action)
    } finally {
      // 有異常,鎖置為false,不影響別的dispatch
      isDispatching = false
    }
    // 執行dispatch,並且更新當前監聽佇列為 最新佇列
    const listeners = (currentListeners = nextListeners)
    // 依次執行,監聽器
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

createStore初始化完成之後會執行dispatch({ type: ActionTypes.INIT }),此時執行初始化操作。

我們要關注下currentState的計算,
將currentState,action傳給reducer處理,然後更新currentState。

針對初始化來說currentState其實就是initState:

// 初始化狀態
let currentState = preloadedState
/****省略***/
// 這裡可以回想下我們reducer中的兩個引數,state和action對應的值
currentState = currentReducer(currentState, action)

reducer示例:

const todos = (state = [], action) => {
switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false
        }
      ]
    }

getSate實現

getState就是獲得store的state。這個比較簡單。當結合react-redux使用時,其會幫我們進行操作。我們就不用自行呼叫這個方法了,所以不要疑惑從哪裡獲取的state。

/**
   * 返回應用當前狀態
   * 不過需要看下當前是否有更新正在進行,是的話則提示
   */
  function getState() {
    // 判斷是否isDispatching 中
    if (isDispatching) {
      throw new Error('省略')
    }
    return currentState
  }

subscribe

subscribe是比較重要的一個方法,用來供我們監聽狀態的變化,以執行相關操作。
例如react-redux中的handleChange 會對是否pure元件及state進行對比,以提升渲染效率。

示例:

 this.unsubscribe = this.store.subscribe(this.handleChange.bind(this))

實現:
返回的是一個函式,可以進行unsubscribe操作。

/** 
   * 訂閱通知
   */
  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
    ensureCanMutateNextListeners()
    // 新增監聽事件
    nextListeners.push(listener)

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

      if (isDispatching) {
        throw new Error(
          '省略'
        )
      }
      // 註冊完成,可以進行取消操作
      isSubscribed = false
      // 保持事件佇列 同步
      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      // 刪除監聽事件
      nextListeners.splice(index, 1)
    }
  }

replaceReducer

這個開發比較少用,用於熱更新

// 用於reducer的熱替換,開發中一般不會直接使用
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }

    currentReducer = nextReducer
    // 更新值之後,進行dispatch。
    dispatch({ type: ActionTypes.REPLACE })
  }

到這裡createStore已經解析完成了,大家應該瞭解該方法到底做了些什麼操作吧。
簡單概括一下就是:接收reducer和initState,返回一個store 物件。該物件提供了監聽、分發等功能,以實現資料的更新。

實際使用中的問題

經過上面的解讀之後,對於redux的常規應用應該有所瞭解了。不過實際使用中可能會遇到些問題。
例如action要求是純物件,而我們獲取資料一般是非同步的,這就需要藉助redux-thunk這個中介軟體了。
actionCreater返回一個函式。如下:

export function func1() {
  return dispatch => {
      dispatch({
      type:'test',
      data:'a'
      })
  }
}

在瞭解如何實現之前,需要先看下redux中介軟體的原理。
因為reducer更多的關注的是資料的操作,對於一些公共的方法,需要抽離出來,不過這些方法在何時使用呢,redux為我們提供了中介軟體來滿足需求。

redux中介軟體原理

redux 借鑑了 Koa裡 middleware 的思想,即鼎鼎大名的洋蔥模型。
帶著問題看redux原始碼

不過這裡請求對應的是dispatch的過程。

每次dispatch的過程中,都要依次將中介軟體執行一遍。
遇到阻塞或者操作完成,執行下箇中介軟體,直到執行完成,以便我們事先日誌,監控、異常上報等功能。
那麼redux 又是如何支援中介軟體的呢。這就離不開applyMiddleware了。
這裡前面的

applyMiddleware實現思路

實現思想比較簡單,通過科裡化和compose,為符合規範的中介軟體分配訪問dispatch和store的途徑,以便在不同階段來自定義資料更新。
例如非同步操作,返回的不是物件,那麼就執行返回的函式,然後呼叫下一個中介軟體。等非同步請求結束,再次dispatch 對應的action。

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.`
      )
    }
    // 賦予每個中介軟體訪問store的能力。
    const middlewareAPI = {
      getState: store.getState,
      // 箭頭函式儲存dispatch,保證其的同步更新
      dispatch: (...args) => dispatch(...args)
    }
    // 串聯中介軟體,並賦予每個中介軟體訪問dispatch的能力。
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 關聯dispatch與中介軟體,組合呼叫之後得到類似下面的新物件
    // dispatch = f1(f2(f3(store.dispatch))));
    dispatch = compose(...chain)(store.dispatch)
    

    return {
      ...store,
      dispatch
    }
  }
}

這樣執行之後返回的,物件就是增強之後的store了。

compose的實現

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

結合redux-thunk示例

redux-thunk原始碼,實現也很優雅,對於返回的function,將dispatch等引數傳遞進去,然後執行,等待回撥非同步完成再dispatch。對於正常物件則進行下一步。

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    // 每次dispatch的時候都會進行判斷,如果是個函式,那就執行函式,不再進行下一步吧,這樣就避免了,函式不滿足action要求的問題
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

那麼實際使用時,在createStore時加入該中介軟體即可:

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk';
const store = createStore(
  reducer,
  applyMiddleware({
  ...middleware,
  thunk})
)

那麼到這裡對於redux的中介軟體 也就是問題2,我想大家也比較清楚了。
對於常見中介軟體可以參考

結束語

參考文章

redux中文文件
深入React技術棧
加上重讀redux原始碼一帶著問題看 react-redux 原始碼實現總算將redux及react-redux重讀了一遍。可能有人會說道這些原始碼,看完也會忘,有這個必要嗎。我感覺分情況來看,如果我們只是使用,那麼看官方文件就可以了,當遇到某些疑問好像找不到貼切解釋的時候,不放一看。
此外也是學習大佬們的設計思路和實現方式,有的放矢才能開卷有益。

相關文章