Redux 學習筆記 - 源碼閱讀

同步自個人 博客react

好久以前就看過一遍 Redux 相關技術棧的源碼,最近在看書的時候發現有些細節已經忘了,並且發現當時的理解有些誤差,打算寫幾篇學習筆記。這是第一篇,主要記錄一下我對 Redux 、redux-thunk 源碼的理解。我會講一下大致的架構,和一些核心部分的代碼解釋,更具體的代碼解釋能夠去看個人 repo,後續會繼續更新 react-redux,以及一些別的 redux 中間件的代碼和學習筆記。git

注意:本文不是單純的講 API,若是不瞭解的能夠先看一下文檔,或者 google 一下 Redux 相關的基礎內容。github

總體架構

在我看來,Redux 核心理念很簡單express

  1. store 負責存儲數據redux

  2. 用戶觸發 action數組

  3. reducer 監聽 action 變化,更新數據,生成新的 storepromise

代碼量也不大,源碼結構很簡單:閉包

.src
    |- utils
    |- applyMiddleware.js
    |- bindActionCreators.js
    |- combineReducers.js
    |- compose.js
    |- createStore.js
    |- index.js

其中 utils 只包含一個 warning 相關的函數,這裏就不說了,具體講講別的幾個函數架構

index.js

這是入口函數,主要是爲了暴露 ReduxAPIapp

這裏有這麼一段代碼,主要是爲了校驗非生產環境下是否使用的是未壓縮的代碼,壓縮以後,由於函數名會變化,isCrushed.name 就不等於 isCrushed

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

createStore

這個函數是 Redux 的核心部分了,咱們先總體看一下,他用到的思路很簡單,利用一個閉包,維護了本身的私有變量,暴露出給調用方使用的 API

// 初始化的 action
export const ActionTypes = {
    INIT: '@@redux/INIT'
}

export default function createStore(reducer, preloadedState, enhancer) {

    // 首先進行各類參數獲取和類型校驗,不具體展開了
    if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
        enhancer = preloadedState
        preloadedState = undefined
  }
  if (typeof enhancer !== 'undefined') {...}
  if (typeof reducer !== 'function') {...}
  
  //各類初始化
  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false
   
  // 保存一份 nextListeners 快照,後續會講到它的目的
  function ensureCanMutateNextListeners() {
        if (nextListeners === currentListeners) {
            nextListeners = currentListeners.slice()
        }
  }
    
  function getState(){...}
  
  function subscribe(){...}
  
  function dispatch(){...}
  
  function replaceReducer(){...}
  
  function observable(){...}
  
  // 初始化
  dispatch({ type: ActionTypes.INIT })

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

下面咱們具體來講

ActionTypes

這裏的 ActionTypes 主要是聲明瞭一個默認的 action,用於 reducer 的初始化。

ensureCanMutateNextListeners

它的目的主要是保存一份快照,下面咱們就講講 subscribe,以及爲何須要這個快照

subscribe

目的是爲了添加一個監聽函數,當 dispatch action 時會依次調用這些監聽函數,代碼很簡單,就是維護了一個回調函數數組

function subscribe(listener) {
    // 異常處理
    ...

    // 標記是否有listener
    let isSubscribed = true

    // subscribe時保存一份快照
    ensureCanMutateNextListeners()
    nextListeners.push(listener)

      // 返回一個 unsubscribe 函數
    return function unsubscribe() {
        if (!isSubscribed) {
            return
        }

        isSubscribed = false
        // unsubscribe 時再保存一份快照
        ensureCanMutateNextListeners()
        //移除對應的 listener
        const index = nextListeners.indexOf(listener)
        nextListeners.splice(index, 1)
    }
}

這裏咱們看到了 ensureCanMutateNextListeners 這個保存快照的函數,Redux 的註釋裏也解釋了緣由,我這裏直接說說個人理解:因爲咱們能夠在 listeners 裏嵌套使用 subscribeunsubscribe,所以爲了避免影響正在執行的 listeners 順序,就會在 subscribeunsubscribe 時保存一份快照,舉個例子:

store.subscribe(function(){
    console.log('first');
    
    store.subscribe(function(){
        console.log('second');
    })    
})
store.subscribe(function(){
    console.log('third');
})
dispatch(actionA)

這時候的輸出就會是

first
third

在後續的 dispatch 函數中,執行 listeners 以前有這麼一句:

const listeners = currentListeners = nextListeners

它的目的則是確保每次 dispatch 時均可以取到最新的快照,下面咱們就來看看 dispatch 內部作了什麼。

dispatch

dispatch 的內部實現很是簡單,就是將當前的 stateaction 傳入 reducer,而後依次執行當前的監聽函數,具體解析大概以下:

function dispatch(action) {
    // 這裏兩段都是異常處理,具體代碼不貼了
    if (!isPlainObject(action)) {
        ...
    }
    if (typeof action.type === 'undefined') {
        ...
    }

    // 立一個標誌位,reducer 內部不容許再dispatch actions,不然拋出異常
    if (isDispatching) {
        throw new Error('Reducers may not dispatch actions.')
    }

    // 捕獲前一個錯誤,可是會將 isDispatching 置爲 false,避免影響後續的 action 執行
    try {
         isDispatching = true
         currentState = currentReducer(currentState, action)
    } finally {
         isDispatching = false
    }

      // 這就是前面說的 dispatch 時會獲取最新的快照
    const listeners = currentListeners = nextListeners
    
    // 執行當前全部的 listeners
    for (let i = 0; i < listeners.length; i++) {
        const listener = listeners[i]
        listener()
    }

    return action
}

這裏有兩點說一下個人見解:

  1. 爲何reducer 內部不容許再 dispatch actions?我以爲主要是爲了不死循環。

  2. 在循環執行 listeners 時有這麼一段

const listener = listeners[i]
listener()

乍一看以爲會爲何不直接 listeners[i]() 呢,仔細斟酌一下,發現這樣的目的是爲了不 this 指向的變化,若是直接執行 listeners[i](),函數裏的 this 指向的是 listeners,而如今就是指向的 Window

getState

獲取當前的 state,代碼很簡單,就不貼了。

replaceReducer

更換當前的 reducer,主要用於兩個目的:1. 本地開發時的代碼熱替換,2:代碼分割後,可能出現動態更新 reducer的狀況

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

      // 更換 reducer
    currentReducer = nextReducer
    // 這裏會進行一次初始化
    dispatch({ type: ActionTypes.INIT })
}

observable

主要是爲 observable 或者 reactive 庫提供的 APIReux 內部並無使用這個 API,暫時不解釋了。

combineReducers

先問個問題:爲何要提供一個 combineReducers

我先貼一個正常的 reducer 代碼:

function reducer(state,action){
    switch (action.type) {
        case ACTION_LIST:
        ...
        case ACTION_BOOKING:
        ...
    }
}

當代碼量很小時可能發現不了問題,可是隨着咱們的業務代碼愈來愈多,咱們有了列表頁,詳情頁,填單頁等等,你可能須要處理 state.list.product[0].name,此時問題就很明顯了:因爲你的 state 獲取到的是全局 state,你的取數和修改邏輯會很是麻煩。咱們須要一種方案,幫咱們取到局部數據以及拆分 reducers,這時候 combineReducers 就派上用場了。

源碼核心部分以下:

export default function combineReducers(reducers) {
    // 各類異常處理和數據清洗
    ... 

    return function combination(state = {}, action) {

        const finalReducers = {};
        // 又是各類異常處理,finalReducers 是一個合法的 reducers map
        ...

        let hasChanged = false;
        const nextState = {};
        for (let i = 0; i < finalReducerKeys.length; i++) {
            const key = finalReducerKeys[i];
            const reducer = finalReducers[key];
            // 獲取前一次reducer
            const previousStateForKey = state[key];
            // 獲取當前reducer
            const nextStateForKey = reducer(previousStateForKey, action);
            
            nextState[key] = nextStateForKey;
            // 判斷是否改變
            hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
        }
        // 若是沒改變,返回前一個state,不然返回新的state
        return hasChanged ? nextState : state;
    }
}

注意這一句,每次都會拿新生成的 state 和前一次的對比,若是引用沒變,就會返回以前的 state,這也就是爲何值改變後 reducer 要返回一個新對象的緣由。

hasChanged = hasChanged || nextStateForKey !== previousStateForKey;

隨着業務量的增大,咱們就能夠利用嵌套的 combineReducers 拼接咱們的數據,可是就筆者的實踐看來,大部分的業務數據都是深嵌套的簡單數據操做,好比我要將 state.booking.people.name 置爲測試姓名,所以咱們這邊有一些別的解決思路,好比使用高階 reducer,又或者即根據 path 來修改數據,舉個例子:咱們會 dispatch(update('booking.people.name','測試姓名')),而後在 reducer 中根據 booking.people.name 這個 path 更改對應的數據。

compose

接受一組函數,會從右至左組合成一個新的函數,好比compose(f1,f2,f3) 就會生成這麼一個函數:(...args) => f1(f2(f3(...args)))

核心就是這麼一句

return funcs.reduce((a, b) => (...args) => a(b(...args)))

拿一個例子簡單解析一下

[f1,f2,f3].reduce((a, b) => (...args) => a(b(...args)))

step1: 由於 reduce 沒有默認值,reduce的第一個參數就是 f1,第二個參數是 f2,所以第一個循環返回的就是 (...args)=>f1(f2(...args)),這裏咱們先用compose1 來表明它

step2: 傳入的第一個參數是前一次的返回值 compose1,第二個參數是 f3,能夠獲得這次的返回是 (...args)=>compose1(f3(...args)),即 (...args)=>f1(f2(f3(...args)))

bindActionCreator

簡單說一下 actionCreator 是什麼

通常咱們會這麼調用 action

dispatch({type:"Action",value:1})

可是爲了保證 action 能夠更好的複用,咱們就會使用 actionCreator

function actionCreatorTest(value){
    return {
        type:"Action",
        value
    }
}

//調用時
dispatch(actionCreatorTest(1))

再進一步,咱們每次調用 actionCreatorTest 時都須要使用 dispatch,爲了再簡化這一步,就可使用 bindActionCreatoractionCreator 作一次封裝,後續就能夠直接調用封裝後的函數,而不用顯示的使用 dispatch了。

核心代碼就是這麼一段:

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

下面的代碼主要是對 actionCreators 作一些操做,若是你傳入的是一個 actionCreator 函數,會直接返回一個包裝事後的函數,若是你傳入的一個包含多個 actionCreator 的對象,會對每一個 actionCreator 都作一個封裝。

export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  //類型錯誤
  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error(
      ...
    )
  }

  // 處理多個actionCreators
  var keys = Object.keys(actionCreators)
  var boundActionCreators = {}
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i]
    var actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

applyMiddleware

想一下這種場景,好比說你要對每次 dispatch(action) 都作一第二天志記錄,方便記錄用戶行爲,又或者你在作某些操做前和操做後須要獲取服務端的數據,這時可能須要對 dispatch 或者 reducer 作一些封裝,redux 應該是想好了這種用戶場景,因而提供了 middleware 的思路。

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 內部先用 createStorereducer 生成了 store,以後又用 store 生成了一個 middlewareAPI,這裏注意一下 dispatch: (action) => dispatch(action),因爲後續咱們對 dispatch 作了修改,爲了保證全部的 middleware 中能拿到最新的 dispatch,咱們用了閉包對它進行了一次包裹。

以後咱們執行了

chain = middlewares.map(middleware => middleware(middlewareAPI))

生成了一個 middleware[m1,m2,...]

再日後就是 applyMiddleware 的核心,它將多個 middleWare 串聯起來並依次執行

dispatch = compose(...chain)(store.dispatch)

compose 咱們以前有講過,這裏其實就是 dispatch = m1(m2(dispatch))

最後,咱們會用新生成的 dispatch 去覆蓋 store 上的 dispatch

可是,在 middleware 內部到底是如何實現的呢?咱們能夠結合 redux-thunk 的代碼一塊兒看看,redux-thunk 主要是爲了執行異步操做,具體的 API 和用法能夠看 github,它的源碼以下:

function createThunkMiddleware(extraArgument) {
    return ({ dispatch, getState }) => next => action => {
        if (typeof action === 'function') {
            return action(dispatch, getState, extraArgument);
        }

        // 用next而不是dispatch,保證能夠進入下一個中間件
        return next(action);
    };
}

這裏有三層函數

  1. ({ dispatch, getState })=> 這一層對應的就是前面的 middleware(middlewareAPI)

  2. next=> 對應前面 compose 鏈的邏輯,再舉個例子,m1(m2(dispatch)),這裏 dispatchm2nextm2(dispatch) 返回的函數是 m1next,這樣就能夠保證執行 next 時能夠進入下一個中間件

  3. action 這就是用戶輸入的 action

到這裏,整個中間件的邏輯就很清楚了,這裏還有一個點要注意,就是在中間件的內部,dispatchnext 是要注意區分的,前面說到了,next 是爲了進入下一個中間件,而因爲以前提到的 middlewareAPI 用到了閉包,若是在這裏執行 dispatch 就會從最一開始的中間件從新再走一遍,若是 middleWare 一直調用 dispatch 就可能致使無限循環。

那麼這裏的 dispatch 的目的是什麼呢?就我看來,其實就是取決與你的中間件的分發思路。好比你在一個異步 action 中又調用了一個異步 action,此時你就但願再通過一遍 thunk middleware,所以 thunk 中才會有 action(dispatch, getState, extraArgument),將 dispatch 傳回給調用方。

小結

結合這一段時間的學習,讀了第二篇源碼依然會有收穫,好比它利用函數式和 curry 將代碼作到了很是精簡,又好比它的中間件的設計,又能夠聯想到 AOPexpress 的中間件。

那麼,redux 是如何與 react 結合的?promisesaga 又是如何實現的?與 thunk 相比有和優劣呢?後面會繼續閱讀源碼,記錄筆記,若是有興趣也能夠 watch 個人 repo 等待後續更新。

相關文章
相關標籤/搜索