Redux 源碼剖析

Redux 源碼剖析

Redux自己只暴露了5個API,分別是:typescript

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
  __DO_NOT_USE__ActionTypes  // redux內部使用的action-type,可忽略
}
複製代碼

這篇文章將每一個api的職責一一作個剖析總結,redux自己可用一句話歸納:redux

可預測的狀態管理容器api

createStore

createStore是redux核心須要調用的,返回一個store對象,包含針對全局狀態樹的「增刪改查」操做,它主要有兩種調用方式:createStore(reducer, preloadState?)createStore(reducer, preloadState?, enhancer?)數組

  • reducer(state, action): 根據指定action返回一個新的state, 更改狀態的核心邏輯所在
  • preloadState: 初始全局狀態(object)
  • enhancer: 一個函數,返回一個增強版的store(主要針對dispatch函數)

後面兩個參數是可選參數,createStore調用後返回一個store對象:markdown

store = {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
}
複製代碼

接下來咱們深刻createStore函數內部,若是enhancer第三個參數存在並是一個函數,或者調用createStore只有兩個參數且第二個參數是個函數,則會有如下邏輯:app

if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    return enhancer(createStore)(reducer, preloadedStat)
}
複製代碼

能夠看到直接調用enhancer並返回了它,返回的就是一個與上面結構一致的增強版store對象,具體enhancer的實現咱們在講到applyMiddlewareAPI的時候在談。若是enhancer不存在的話,就會執行如下邏輯(簡單起見我寫了個簡易版的實現,其餘具體會在後面一塊兒講解源碼的設計細節):less

function createStore(reducer, preloadState) {
    let state = preloadState
    let listeners = []
    function getState() {
        return state
    }
    function subscribe(listener) {
        listeners.push(listener)
        return function unsubscribe() {
            var index = listeners.indexOf(listener)
            listeners.splice(index, 1)
        }
    }
    function dispatch(action) {
        state = reducer(state, action)
        listeners.forEach(listener => listener())
        return action
    }
    dispatch({})
    return { dispatch, subscribe, getState }
}
複製代碼
  • getState(): 返回當前的全局狀態對象
  • subscribe(listener): 訂閱一個監聽器(函數)
  • dispatch(action): 根據指定的action執行對應的reducer邏輯並返回最新的state,而後執行訂閱的全部監聽器,也是更改全局狀態的惟一方法

在listener回調函數中須要調用store.getState()拿到最新的state,在執行其餘邏輯,關於爲何不把state當成參數直接給每一個listener回調,能夠看看這個FAQ,上面就是redux的最原始簡單實現,你們應該都能看懂,但確定上不了生產的,有不少注意點須要專門提出來講下函數

首先在reducer中能夠再次進行dispatch調用嗎?源碼設計中是不能夠的,顯然若是能夠在reducer中再次執行dispatch操做,對性能是一個很大的隱患,會屢次對listeners中的監聽器進行屢次渲染,所以社區也有不少插件其實也是能夠進行批量dispatch的,例如redux-batch, redux-batched-actions,這些都是上面提到的提供了enhancer函數也就是一個增強版的dispatch函數,後續會提到enhancer。所以,redux的dispatch方法源碼中有如下邏輯:工具

const isDispatching = false
function dispatch(action) {
    // ...
    if (isDispatching) {
        throw new Error('Reducers may not dispatch actions.')
    }

    try {
        isDispatching = true
        state = reducer(state, action)
    } finally {
        isDispatching = false
    }
    // ...
}
複製代碼

值得一提的是isDispatching變量也在subscribeunsubscribe中使用了,也就是說,在reducer中也是不能進行store.subscribe()和取消訂閱操做的。oop

在想一下,能夠在listener監聽函數中再次執行store.subscribe()嗎?想一下應該是能夠的,但是看下咱們上面的簡易實現,若是在forEach循環的listener中再次執行listeners.push(listener)或者調用相應的unsubscribe函數可能會致使bug,由於push和splice操做都是改變了原數組。顯然,這裏須要兩個listeners來防止這種狀況出現,在源碼中聲明瞭兩個變量以及一個函數:

let currentListeners = []
let nextListeners = currentListeners

// 拷貝一份currentListeners到nextListeners
function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
}
複製代碼

而後在subscribe函數體中,以及返回的unsubscribe中:

function subscribe(listener) {
    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe() {
        ensureCanMutateNextListeners()
        const index = nextListeners.indexOf(listener)
        nextListeners.splice(index, 1)
        currentListeners = null
    }
}
複製代碼

這裏訂閱和取消訂閱操做都是在nextListeners上進行的,那麼dispatch中就確定須要在currentListeners中進行循環操做:

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

如此設計就會避免相應的bug,但這樣有一個明顯的點要記着的就是,每次在listener回調執行訂閱操做的一個新listener不會在這次正在進行的dispatch中調用,它只會在下一次dispatch中調用,能夠看做是在dispatch執行前的listeners中已經打了個快照了,此次的dispach調用中在listener回調中新增的listener只能在下個dispatch中調用,由於currentListeners裏尚未最新的listener呢

能夠看到store中還返回了一個replaceReducer方法,直接粘貼源碼以下:

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

    dispatch({ type: ActionTypes.REPLACE })
    return store
}
複製代碼

replaceReducer方法自己的邏輯就如此簡單,正如字面意思就是替換一個新的reducer,dispatch({ type: ActionTypes.REPLACE })這行與上面的簡版代碼dispatch({})效果相似,每當調用replaceReducer函數時都會以新的ruducer初始化舊的state併產生一個新的state,它比較經常使用於動態替換reducer或者實現熱加載時候使用

compose

compose函數在redux中的職責更像是一個工具函數,能夠把它想象成一個組合器,專門將多個函數組合成一個函數調用,源碼也很簡單,以下:

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)))
}
複製代碼

它主要是利用了數組的reduce方法將多個函數匯聚成一個函數,可將它看做成這個操做:compose(a, b ,c) = (...arg) => a(b(c(...arg))),從這個形式能夠看出傳給compose函數的參數必須都是接受一個參數的函數,除了最右邊的函數(即以上的c函數)能夠接受多個參數,它暴露出來成一個獨立的api多用於組合enhancer

applyMiddleware

applyMiddleware函數能夠說是redux內部的一個enhancer實現,它能夠出如今createStore方法的第二個參數或第三個參數調用createStore(reducer, preloadState, applyMiddleware(...middlewares)),redux相關插件基本都要通過它之口。它接受一系列的redux中間件applyMiddleware(...middlewares)並返回一個enhancer,咱們先來看下它的源碼:

function applyMiddleware(...middlewares) {
  return createStore => (reducer, ...args) => {
    const store = createStore(reducer, ...args)
    let dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      )
    }
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action, ...args) => dispatch(action, ...args)
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}
複製代碼

以上就是applyMiddleware的全部源碼,看redux源碼的全部api設計就有個感受,看起來都很精簡。咱們從它的參數看起,它接受一系列的middleware,redux官方說middleware的設計應遵循這樣的簽名({ getState, dispatch }) => next => action,爲何要這樣設計呢?咱們一步一步來看,先看下它的返回值,它返回的是一個函數,相似這樣的簽名createStore => createStore,從以上代碼來看相似這種createStore => (reducer, ...args) => store,因此這也經常被用做createStore的第三個參數存在,還記得咱們在講createStore函數時說了,如果enhancer存在會有以下邏輯:

if (typeof enhancer !== 'undefined') {
  if (typeof enhancer !== 'function') {
    throw new Error('Expected the enhancer to be a function.')
  }
  return enhancer(createStore)(reducer, preloadedStat)
}
複製代碼

恰好對應於咱們上面寫的函數簽名,也就是說若是enhancer不存在,redux會建立內部的store,若是存在,就先建立本身內部的store,而後將store傳給中間件進行「魔改」,「魔改「什麼呢?dispatch函數,看一下它最後的返回值return { ...store, dispatch }覆蓋了原生的dispatch方法,但並非說原生的dispatch方法不見了,它只是通過中間件而被加工賦予了更多的功能,接着往下看最核心的兩行

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

從第一行很容易能夠看出它執行了衆多中間件,以getState和dispatch方法爲命名參數傳遞給中間件,對應於咱們上面說的這樣的簽名({ getState, dispatch }) => xxx,這樣上面的chain常量就應該是這個樣子[next => action => {}, next => action => {}, xxx],最關鍵的就是上面第二行代碼了,compose函數咱們已經解釋過了,假設chain常量是這樣[a, b],那麼就會有以下代碼:

dispatch = ((...args) => a(b(...args)))(store.dispatch)
複製代碼

看了上面的代碼,可能有的人以爲看着更復雜了,其實compose(...chain) = (...args) => a(b(...args)),而後就是個當即執行函數

(a => {
  console.log(a)  // 當即打印 1
})(1)
複製代碼

而後從上面咱們就能夠得出這塊代碼:dispatch = a(b(store.dispatch))這裏咱們就要去解釋爲何middleware要設計成這樣({ getState, dispatch }) => next => action,咱們在進行一步一步拆解以下:

b(store.dispatch) = action => {
  // store.dispatch可在此做用域使用,即 next
}
複製代碼

action => {}不就是redux中dispatch的函數簽名嘛,因此b(store.dispatch)就被當成一個新的dispatch傳遞給a(),a在以同種方式循環下去最終賦給dispatch,值得注意的是每一箇中間件最終返回值應這樣寫return next(action),最終

dispatch = action => next(action)
複製代碼

action變量信息就會隨着next一步步從a到b穿透到最後一箇中間件直至被redux內部的store.dispatch調用,也就是最終修改reducer邏輯,store.dispatch最終返回的仍是action,這就是中間件邏輯,你能夠在中間件中任什麼時候候調用next(action),最終返回它就好了,咱們能夠看個redux官網的小例子:

function logger({ getState }) {
  return next => action => {
    console.log('will dispatch', action)
    // Call the next dispatch method in the middleware chain.
    const returnValue = next(action)
    console.log('state after dispatch', getState())

    // This will likely be the action itself, unless
    // a middleware further in chain changed it.
    return returnValue
  }
}

const store = createStore(todos, ['Use Redux'], applyMiddleware(logger))

store.dispatch({
  type: 'ADD_TODO',
  text: 'Understand the middleware'
})
// (These lines will be logged by the middleware:)
// will dispatch: { type: 'ADD_TODO', text: 'Understand the middleware' }
// state after dispatch: [ 'Use Redux', 'Understand the middleware' ]
複製代碼

combineReducers

combineReducers也能夠算個工具函數,它旨在把一個以對象形式的多個reducer合併成一個reducer傳給createStore方法,相似以下:

const reducer = combineReducers({
  foo: (fooState, action) => newFooState,
  bar: (barState, action) => newBarState,
  ...,
})
const store = createStore(reducer)
複製代碼

在一些大型複雜場景中應用仍是挺普遍的,可將全局狀態分離成一個字狀態方便維護,咱們來看下它的源碼實現:

function combineReducers(reducers) {
  const finalReducers = { ...reducers }
  const finalReducerKeys = Object.keys(finalReducers)

  return function combination(state = {}, action) {
    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
    }
    hasChanged =
      hasChanged || finalReducerKeys.length !== Object.keys(state).length
    return hasChanged ? nextState : state
  }
}
複製代碼

省去了一些類型判斷和報錯信息的邏輯,只保留了核心的實現。關於它的入參和返回值上面已經說過了,咱們着重來看下返回的combination實現,當你在業務代碼中每次dispatch一個action的時候,這最終的combination reducer就會循環遍歷子reducer,從for 循環中const nextStateForKey = reducer(previousStateForKey, action)就能夠看出來它將計算出的子新state存在nextState中,這裏有個點要注意的就是咱們的子reducer須要處理傳入的state爲undefined的狀況(state的默認值是{}),並且子reducer的返回值也不能是undefind,常見的處理狀況就給個默認值就行(state = initialState, action) => xxx

還注意到hasChanged變量的做用,它在每次的for 循環中只要返回的新state與舊state不一樣就爲true,循環外還判斷了下整個過程有沒有新增或刪除的reducer,爲true就返回新的nextState,false返回原有state,這基本上就是combineReducers的實現邏輯,也不復雜

bindActionCreators

bindActionCreators函數也算是個工具函數,瞭解了上面的api源碼結構,看它的做用也就知道了,以下:

function bindActionCreator(actionCreator, dispatch) {
  return function(this, ...args) {
    return dispatch(actionCreator.apply(this, args))
  }
}

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

  const boundActionCreators = {}
  for (const key in actionCreators) {
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}
複製代碼

這個跟隨官方的小例子,一看就會明白怎麼使用了~

總結

redux源碼很好讀~尤爲如今仍是用typescript書寫,各個函數的接口類型都一清二楚,我的以爲比較複雜難想的就是中間件的設計了,不過從頭看下來能感受到源碼的短小精煉,其中也是收穫蠻多~


原文連接

相關文章
相關標籤/搜索