一篇帶有「思考」的redux源碼閱讀

此次閱讀的redux的版本是4.x版本,爲啥不是最新的呢?由於最新的redux使用typescript來重寫了,變化不是特別大,而typescript會有不少的函數類型定義;另外一方面,對js的熟練度確定比ts要好,理解起來也會相對容易一點,還有此次閱讀源碼的目的是爲了瞭解整個redux的工做流程。redux的源碼很是精簡,很適合源碼的閱讀與學習。在閱讀以前,對redux有必定的使用會帶來更好的效果,一邊看一邊反思日常所寫的內容。下面就開始吧:react

目錄

下面的內容是項目的src目錄結構git

# src
-- utils/
  -- actionTypes.js
  -- isPlainObject.js
  -- warning.js
-- applyMiddleware.js
-- bindActionCreators.js
-- combineReducers.js
-- compose.js
-- createStore.js
-- index.js
複製代碼

項目的文件很是的少,主要邏輯也是在src直接目錄下的文件,先作個熱身,對簡單的utils文件夾入手,這是一些通用的工具方法:github

utils

在看源碼的過程當中,對一些工具方法的使用效果保持必定的記憶,對流程的理解上挺有幫助typescript

actionTypes.js

const ActionTypes = {
  INIT: `@@redux/INIT${randomString()}`,
  REPLACE: `@@redux/REPLACE${randomString()}`,
  PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
}
複製代碼

actionTypes.js主要定義一些redux內部使用的actionrandomString函數是生成一些隨機字符串,保證內部使用的action不會衝突redux

isPlaginObject.js

export default function isPlainObject(obj) {
  if (typeof obj !== 'object' || obj === null) return false

  let proto = obj
  while (Object.getPrototypeOf(proto) !== null) {
    proto = Object.getPrototypeOf(proto)
  }

  return Object.getPrototypeOf(obj) === proto
}
複製代碼

該方法用於判斷參數是不是一個「純對象」。什麼是「純對象」,就是直接繼承Object.prototype的對象,例如直接聲明的對象:const obj = {};若是const objSub = Object.create(obj),那麼objSub就不是這裏說的「純對象」。數組

warning.js

export default function warning(message) {
  if (typeof console !== 'undefined' && typeof console.error === 'function') {
    console.error(message)
  }
  try {
    throw new Error(message)
  } catch (e) {}
}
複製代碼

warning.js邏輯比較簡單:先把錯誤的詳細信息打印出來,再拋出錯誤。緩存

核心

熱身完以後,咱們來看一下redux的核心,入口在:src/index.js:bash

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

這個是index.js的暴露對象,都是從外部引入;除此以外,還有一個叫空函數isCrushed閉包

function isCrushed() {}
複製代碼

這個空函數的做用是啥?由於在代碼壓縮的時候,會對該函數進行重命名,變成function a(){},這樣子的函數;這個函數的做用就是,判斷若是redux代碼被壓縮了,並且redux不是運行在production環境,就會報錯,提示使用開發版本。app

redux 的核心是createStore,這個核心咱們先放一下,後面再處理,先了解一些輔助該核心的方法:

bindActionCreators

這個方法出場率有時候不是很高,那麼它的做用是啥?

首先咱們知道一個詞彙actionCreator,這個actionCreator就如命名那樣,是用於建立action類型的函數。那bindActionCreators的目的又是什麼?這裏可能要結合react-reduxconnect方法與「容器組件」、「展現組件」(容器組件 vs 展現組件)來講明會更好。

一般狀況下,若是要讓當前組件是用redux,咱們會使用react-reduxconnect方法,把咱們的組件經過connect包裹爲一個高級組件,而包裹的過程拿到dispatch與咱們指定的store數據:

// Container.jsx
import { connect } from 'react-redux'

class Container extends React.Component {
    // ...
    render () {
        return <SimpleComponent /> } } export default connect(state => ({ todo: state.todo }))(Container) 複製代碼

而這個Container組件咱們能夠稱之爲「容器組件」,由於裏面包含了一些複雜的處理邏輯,例如與redux的鏈接;而若是SimpleComponent組件也有一些操做,這些操做須要更改到redux的內容,這樣子的話,處理方法有兩個:

  1. SimpleComponent也使用connect處理爲高級組件
  2. Containerreduxdispatch方法顯示傳遞到SimpleComponent

這兩個方法都不是很好,第一方法會讓組件更加複雜,可能與咱們的容器組件-展現組件的姿式有點不一樣;第二種方法也能夠,可是會讓組件變得耦合度高。那能不能讓SimpleComponent組件給Container的反饋也經過日常props-event的形式來處理呢,讓SimpleComponent感知不到redux的存在?

這個時候就可使用bindActionCreators了;例若有一個action爲:{ type: 'increment', value: 1},一般若是Container組件觸發能夠經過:

// actions.js
const Add = (value) => { type: 'increment', value }
複製代碼
// Container.jsx
import Add from './actions.js'

// Container.jsx 某個事件觸發觸發更新
class Container extends React.Component {
    onClick() {
        dispatch(Add(1))
     }
}
複製代碼

利用bindActionCreators處理後,給到SimpleComponent使用則能夠這樣:

// Container.jsx
import Add from './actions.js'

class Container extends React.Component {
    render () {
        const { dispatch } = this.props // 經過 react-redux 的 connect 方法組件能夠獲取到
        const actions = bindActionCreator({
            add: Add
        }, dispatch)
        
        return <SimpleComponent {...actions} /> } } // SimpleComponent.jsx function SimpleComponent({ add }) { return <button onClick={() => add(1)}>click</button> } 複製代碼

經過bindActionCreators處理後的函數,add,直接調用,就能夠觸發dispatch來更新,而這個時候SimpleComponent並無感知到有redux,只是當是一個事件函數那樣子調用。

image.png

瞭解到bindActionCreators的做用以後,咱們再來看一下源碼就很好理解了:

function bindActionCreator(actionCreator, dispatch) {
  // 使用閉包來屏蔽 dispatch 與 actionCreator
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}
export default function bindActionCreators(actionCreators, dispatch) {
  // 當 actionCreators 只有一個的時候,直接返回該函數的打包結果
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  // 省略參數類型判斷
  // ...
  const boundActionCreators = {}
  for (const key in actionCreators) {
    const actionCreator = actionCreators[key]
    // 只對 { key: actionCreator } 中的函數處理;actionCreators 中的其餘數據類型被忽略
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}
複製代碼

combineReducers

接下來講一下combineReducers,這個方法理解起來比較簡單,就是把多個reducer合併到一塊兒,由於在開發過程當中,大多數的數據不會只有一個reducer這麼簡單,須要多個聯合起來,組成複雜的數據。

// () => {} 爲每一個對應的reducer
const state = {
    count: () => {},
    userData: () => {},
    oeherData: () => {}
}
複製代碼

一般使用compineReducer可讓咱們規避一些問題,例如對reducer的傳入參數的判斷等,保證reduce流程的運轉,簡化核心代碼以下,去掉一部分開發代碼,可是會註釋做用:

export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  // 檢查全部reducer的key值是否合法,reducer是一個函數,則加入到finalReducers
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]

    // 這裏有個判斷,若是reducers對象的某個key值的value爲undefined,則報錯

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers)

  // 這裏有個判斷(assertReducerShape),判斷是否有 reducer 返回是 undefined
  // 若是有,則先保留這個錯誤,咱們定義爲錯誤 A
    
  // 這個 combination 函數,每次dispatch都會執行一次
  return function combination(state = {}, action) {
    // 這裏有個判斷,若是錯誤A存在,則拋出異常

    // 這裏有個判斷(getUnexpectedStateShapeWarningMessage),會對數據進行多重判斷,
    // 判斷有錯,則拋出異常,判斷的規則有:
    // 1. reducers的數量是否爲0
    // 2. 對每次執行reducer傳入的state(state的來源後面講到)是不是「純對象」(上面有提到)
    // 3. 對每次執行reducer傳入的state對象判斷,是否該對象全部的字段都是「本身的」(hasOwnProperty),
    // 也就是沒有一些從父對象繼承,toString ?
    // 第三點其實有點不太瞭解,由於第二步純對象已通過濾了?
    
    // 下面這個就是 combineReducers 的核心代碼
    let hasChanged = false
    const nextState = {}
    
    // 遍歷全部的函數reducer,獲取返回值,經過判斷先後值的不一樣,判斷是否發生了變化,有變化,則返回新的state
    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)
      
      // 不容許 reducer 返回的值爲 undefined,不然報錯
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      // 這裏判斷是否改變,是經過判斷 reducer 返回的值與以前的值是否一致
      // 因此就突出了「不可變對象」的重要性
      // 若是reducer每次返回的對象是在舊對象上面更改數據
      // 而對象地址沒改變,那麼 redux 就認爲,此次改變是無效的
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
  }
}
複製代碼

createStore

講完上面兩個輔助方法以後,來說一下建立store的核心createStore的方法;由於createStore方法比較長,下面先看一下概覽:

export default function createStore(reducer, preloadState, enhancer) {
    // 判斷是否傳入各類參數是否符合要求
    // 對於加強器(enhancer)的調用會提早返回
    // 建立store的過程被延後到加強器中
    // ...
    
    // 當前最新的 reducer 與 state
    // listeners 是經過 store實例subscribe的函數數組
    let currentReducer = reducer
    let currentState = preloadedState
    let currentListeners = []
    let nextListeners = currentListeners
    // 當前的reducer是否在執行當中
    let isDispatching = false
    
    // 用於防止 listeners 數組出錯,後面講到
    function ensureCanMutateNextListeners() {}
    // 返回當前最新的 state,給外部函數調用獲取內部數據
    function getState() {
        return currentState
    }
    
    // store.subscribe的方法,用於添加 listener,後面有詳細講解
    // 監聽 state 的變化
    function subscribe(listener) {}
    
    // 觸發 reducer 執行,返回新的 state
    function dispatch(action) {}
    
    // 使用新的 reducer 替換當前的 currentReducer
    // 一般在兩種狀況下使用:
    // 1. 部分 reducer 異步加載,加載完畢後添加
    // 2. 用於開發時候用,熱更新
    function replaceReducer(nextReducer) {
        currentReducer = nextReducer
    }
    
    // TODO 這個瞭解很少
    function observable () {}
    
    // 觸發第一個更新。拿到第一次初始化的 state
    dispatch({ type: ActionTypes.INIT })
}
複製代碼

在沒有enhancer處理的過程,createStore的過程,都是一些聲明的函數與變量,惟一開始執行的是dispatch,如今就從這個dispatch開始講解:

function dispatch (action) {
    // 判斷 action 是不是「純」函數
    // 判斷 action.type 是否存在
    // ...
    
    // 判斷當前的dispatch是否在執行中,屢次觸發,則報錯
    if (isDispatching) { throw new Error() }
    
    try {
      isDispatching = true
      // 嘗試去執行 reducer,把返回的 state 做爲最新的 state
      // 若是 咱們的 reducer 是使用 combineReducers 方法包裹的話
      // 這裏的 currentReducer 爲 comineReducer的combination方法
      // 這裏回答了以前所說的 combination 方法拿到的第一個參數 state
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }
    
    // 更新完 state 以後,就會把監聽的函數全都執行一遍
    // 注意這裏的 currentListeners 被賦值爲 nextListeners
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
}
複製代碼

整個 dispatch 就結束了,很簡單,就是把全部reducer都執行一遍,返回最新的 reducer;若是使用combineReducer來聯合全部的reducer的話,至關於執行combination方法,該方法會把被聯合的全部reducer都執行一遍,因此這裏能解釋說,爲何在reducer方法的時候,在switch...case要保留一個default選項,由於有可能執行當前reducer的action,是用於觸發其餘reducer的;這種狀況就把當前reducer對應的state返回便可

function reducer(state, action) {
    switch (action.type) {
    case '':
        // ...
        break
    case '':
        // ...
        break
    default:
        return state
    }
}
複製代碼

當state經過reducer更新以後,就會把加入監聽的listener逐個執行;循環的listenerscurrentListeners,這裏要圓一下以前說的ensureCanMutateNextListeners函數與subscribe的行爲,函數代碼爲:

function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
        nextListeners = currentListeners.slice()
    }
}

function subscribe(listener) {
    // 省略部分參數與進程判斷
    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)
    }
}
複製代碼

咱們看到subscribeunsubscribe的過程,只是一個很簡單的數組處理添加與刪除listeners的過程,可是這兩個過程都有執行ensureCanMutateNextListeners的函數。這個函數的做用是:

保證當次觸發listeners的過程不受影響

這句話怎麼理解呢?能夠看到觸發listeners也只是把listeners的函數循環執行一遍。但若是listeners由此至終都只是一個數組,那麼若是某個listeners執行的內容,再次添加/刪除listener,那麼這個循環過程就有可能出現問題:

const { dispatch, subscribe } = createStore(/* ... */)

subscribe(() => {
    log('state change')
    // 在 listeners 添加監聽方法
    subscribe(() => {
        
    })
    // 或者 移除以前監聽的部分方法
    unsubscribe(/* ... */)
})
複製代碼

因此ensureCanMutateNextListenerslisteners區分爲兩個數組,一個是當前循環的數組,另外一個是下次循環的數組。每次觸發dispatch都是最近更新的一次listeners快照。

compose 與 applyMiddleware

瞭解完核心createStore以後,咱們再瞭解一下加強核心功能的函數:applyMiddleware,由於applyMiddlewarecompose關聯很密切,applyMiddleware的實現依賴compose

compose

compose是一個函數,先看一下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的代碼的做用是簡化代碼:

(...args) => f(g(h(...args)))

// 等同於 
compose(f, g, h)(...args)

// compose使用例子
function foo (str) {
    return str + '_foo'
}
function bar (str) {
    return str + '_bar'
}
function baz (str) {
    return str + '_baz'
}
compose(baz, bar, foo)('base') // "base_foo_bar_baz"
複製代碼

compose方法就是把上一個函數執行的結果做爲下一個函數執行的參數,執行順序從後往前,傳入參數的最後一個函數先被執行。

applyMiddleware

middleware就是一箇中間件的概念,簡化以下:

image.png

數據通過每一箇中間件的處理,會對數據,或者保留一些數據的痕跡,例如寫入日誌等

applyMiddleware的用法也是相似:

const store = createStore(rootReducer, {}, applyMiddleware(middleware1, middleware2))
複製代碼
// applyMiddleware 源碼
export default function applyMiddleware(...middlewares) {
    // createStore方法做爲參數傳入
    // 至關於延遲一步初始化 store
    return createStore => (...args) => {
        const store = createStore(...args)
        let dispatch = () => {/* ... */}

        const middlewareAPI = {
            getState: store.getState,
            dispatch: (...args) => dispatch(...args)
        }
        
        // 把傳入的 middleware 先執行了一遍
        // 把 getState 與 dispatch 方法傳入
        // 讓 middleware 可以獲取到當前 store 的 state與有觸發新的 dispatch 能力
        const chain = middlewares.map(middleware => middleware(middlewareAPI))
        
        // 這個時候的 dispatch 不是原有的 createStore 函數中的方法
        // 而是一個通過 middleware 集成的新方法
        // 而原有的 dispatch 方法做爲參數,傳入到不一樣的middleware
        dispatch = compose(...chain)(store.dispatch)

        return {
            ...store,
            // 使用當前的 dispatch 覆蓋 createStore 的 dispatch 方法
            dispatch
        }
  }
}
複製代碼

redux-thunk是一個對於瞭解middleware很好的例子,下面參照redux-thunk弄一個自定義的middleware, 源碼以下:

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

爲何會函數嵌套那麼多層呢?其實每一層都是有緣由的;第一層:

function customeMiddleware ({dispatch, getState}) {
    // ...
}
複製代碼

dispatch是可以觸發一個完整流程更改state的方法,getState方法用於獲取整個reducer的state數據;這兩個方法都是給到middleware須要獲取完整state的方法。從上面applyMiddleware的方法能夠知道,applyMiddleware執行的時候,就先把middleware函數都執行了一遍,返回chains數組:

const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
複製代碼

看到這裏,會有一個疑問compose執行的順序是從後面往前執行,可是咱們定義middleware是從前日後的。

chains數組的方法至關於middlewarenext方法(接收參數爲next函數,暫時這樣子命名),compose執行的時候,至關於next已經執行,而且返回一個新的函數,這個函數是接收一個叫action的函數(暫命名爲action函數);由於每一個middleware的接收next函數執行後都是action函數;next函數的next參數就是上一個函數的返回值。執行到最後,dispatch = compose(...)(store.dispatch),dispatch函數實際上是第一個middlewareaction函數

// chain0 表示 chain 數組中的第一個函數,chain1表示第二個,以此類推
// compose 執行順序爲倒序
const action2 = chain2(store.dispatch) // store.dispatch的值是compose()(store.dispatch)傳入的
const action1 = chain1(action2)
const action0 = chain0(action1)
複製代碼

action0就是最終返回到dispatch函數;當咱們在組件中執行dispatch()的時候,其實是調用action0函數,action0函數能夠經過next調用下一個middleware

// action0
action0(action)
const result = next(action) // 這個 next 的函數爲 action1
    // action1
    action1(action)
    const result = next(action) // 這個 next 的函數爲action2
        // action2
        action2(action)
        const result = next(action) // 這個 next 的函數爲 store.dispatch
複製代碼

就這樣子層層嵌套,把每一個middleware都執行完,最終去到store.dispatch,最終更改好reducer,返回一個全新的state;而這個state也層層冒泡傳到最頂層的middlewaremiddleware執行順序的疑問由此解開。

小結

redux的源碼很少,使用起來也很簡單,可是裏面運用的知識很多,特別是在middleware的時候,須要很細心的看且有較好的基礎,否則看起來仍是有點吃力的。另一些用閉包來緩存變量、保存函數執行狀態等,用得很精妙。Get.~

安利:若是你們以爲文章不錯,能夠在github給個star,更多文章,能夠看個人 github issues

相關文章
相關標籤/搜索