Redux源碼解讀

前言

作為React全家桶的一份子,Redux為react提供了嚴(yán)謹(jǐn)周密的狀態(tài)管理昼浦。但Redux本身是有點(diǎn)難度的搬设,雖然學(xué)習(xí)了React也有一段時(shí)間了,自我感覺(jué)算是入了門(mén)膀捷,也知道redux的大概流程迈嘹。但其背后諸如creatstore,applymiddleware等API背后到底發(fā)生了什么事情,我其實(shí)還是不怎么了解的全庸,因此最近花了幾天時(shí)間閱讀了Redux的源碼秀仲,寫(xiě)下文章紀(jì)錄一下自己看源碼的一些理解。

一壶笼、源碼結(jié)構(gòu)(redux4.0版本)

Redux是出了名的短小精悍(恩神僵,這個(gè)形容很貼切),只有2kb大小覆劈,且沒(méi)有任何依賴(lài)保礼。它將所有的臟活累活都交給了中間件去處理,自己保持著很好的純潔性责语。再加上redux作者在redux的源碼上炮障,也附加了大量的注釋?zhuān)虼藃edux的源碼讀起來(lái)還是不算難的。

先來(lái)看看redux的源碼結(jié)構(gòu)坤候,也就是src目錄下的代碼:

源碼結(jié)構(gòu)

其中utils是工具函數(shù)铝阐,主要是作為輔助幾個(gè)核心API,因此不作討論铐拐。
(注:由于篇幅的問(wèn)題徘键,下面代碼很多都刪除了官方注釋?zhuān)洼^長(zhǎng)的warn)

二练对、具體組成

index.js是redux的入口函數(shù)具體代碼如下:

2.1 index.js

import createStore from './createStore'
import combineReducers from './combineReducers'
import bindActionCreators from './bindActionCreators'
import applyMiddleware from './applyMiddleware'
import compose from './compose'
import warning from './utils/warning'
import __DO_NOT_USE__ActionTypes from './utils/actionTypes'

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

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
  __DO_NOT_USE__ActionTypes
}

其中isCrushed函數(shù)是用于驗(yàn)證在非生產(chǎn)環(huán)境下 Redux 是否被壓縮,如果被壓縮就會(huì)給開(kāi)發(fā)者一個(gè) warn 的提示吹害。

在最后index.js 會(huì)暴露 createStore, combineReducers, bindActionCreators, applyMiddleware, compose 這幾個(gè)redux最主要的API以供大家使用螟凭。

2.2 creatStore

createStore函數(shù)接受三個(gè)參數(shù):

  • reducer:是一個(gè)函數(shù),返回下一個(gè)狀態(tài)它呀,接受兩個(gè)參數(shù):當(dāng)前狀態(tài) 和 觸發(fā)的 action螺男;
  • preloadedState:初始狀態(tài)對(duì)象,可以很隨意指定纵穿,比如服務(wù)端渲染的初始狀態(tài)下隧,但是如果使用 combineReducers 來(lái)生成 reducer,那必須保持狀態(tài)對(duì)象的 key 和 combineReducers 中的 key 相對(duì)應(yīng)谓媒;
  • enhancer:是store 的增強(qiáng)器函數(shù)淆院,可以指定為中間件,持久化 等句惯,但是這個(gè)函數(shù)只能用 Redux 提供的 applyMiddleware 函數(shù)來(lái)進(jìn)行生成

下面就是creactStore的源碼土辩,由于整體源碼過(guò)長(zhǎng),且 subscribe 和 dispatch 函數(shù)也挺長(zhǎng)的抢野,所以就將 subscribe 和 dispatch 單獨(dú)提出來(lái)細(xì)講拷淘。

 import $$observable from 'symbol-observable'

import ActionTypes from './utils/actionTypes'
import isPlainObject from './utils/isPlainObject'

export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }
  // enhancer應(yīng)該為一個(gè)函數(shù)
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    //enhancer 接受 createStore 作為參數(shù),對(duì)  createStore 的能力進(jìn)行增強(qiáng)指孤,并返回增強(qiáng)后的  createStore 启涯。
    //  然后再將  reducer 和  preloadedState 作為參數(shù)傳給增強(qiáng)后的  createStore ,最終得到生成的 store
    return enhancer(createStore)(reducer, preloadedState)
  }
  // reducer必須是函數(shù)
  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }

 // 初始化參數(shù)
  let currentReducer = reducer   // 當(dāng)前整個(gè)reducer
  let currentState = preloadedState   // 當(dāng)前的state,也就是getState返回的值
  let currentListeners = []  // 當(dāng)前的訂閱store的監(jiān)聽(tīng)器
  let nextListeners = currentListeners // 下一次的訂閱
  let isDispatching = false // 是否處于 dispatch action 狀態(tài)中, 默認(rèn)為false

  // 這個(gè)函數(shù)用于確保currentListeners 和 nextListeners 是不同的引用
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  // 返回state
  function getState() {
    if (isDispatching) {
      throw new Error(
        ......
      )
    }
    return currentState
  }

  // 添加訂閱
  function subscribe(listener) {
  ......
    }
  }
// 分發(fā)action
  function dispatch(action) {
    ......
  }

  //這個(gè)函數(shù)主要用于 reducer 的熱替換恃轩,用的少
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }
    // 替換reducer
    currentReducer = nextReducer
    // 重新進(jìn)行初始化
    dispatch({ type: ActionTypes.REPLACE })
  }

  // 沒(méi)有研究结洼,暫且放著,它是不直接暴露給開(kāi)發(fā)者的详恼,提供了給其他一些像觀察者模式庫(kù)的交互操作。
  function observable() {
    ......
  }

  // 創(chuàng)建一個(gè)store時(shí)的默認(rèn)state
  // 用于填充初始的狀態(tài)樹(shù)
  dispatch({ type: ActionTypes.INIT })

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

subscribe
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 和 currentListeners 是一個(gè)引用引几,重新復(fù)制一個(gè)新的
    ensureCanMutateNextListeners()
    nextListeners.push(listener)

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

      if (isDispatching) {
        throw new Error(
          .......
        )
      }
      
      isSubscribed = false
      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      // 從nextListeners里面刪除昧互,會(huì)在下次dispatch生效
      nextListeners.splice(index, 1)
    }
  }

有時(shí)候有些人會(huì)覺(jué)得store.subscribe用的很少,其實(shí)不然,是react-redux隱式的為我們幫我們完成了這方面的工作伟桅。subscribe函數(shù)可以給 store 的狀態(tài)添加訂閱監(jiān)聽(tīng)敞掘,一旦我們調(diào)用了 dispatch來(lái)分發(fā)action ,所有的監(jiān)聽(tīng)函數(shù)就會(huì)執(zhí)行楣铁。而 nextListeners 就是儲(chǔ)存當(dāng)前監(jiān)聽(tīng)函數(shù)的列表玖雁,當(dāng)調(diào)用 subscribe,傳入一個(gè)函數(shù)作為參數(shù)時(shí)盖腕,就會(huì)給 nextListeners 列表 push 這個(gè)函數(shù)赫冬。同時(shí)調(diào)用 subscribe 函數(shù)會(huì)返回一個(gè) unsubscribe 函數(shù)浓镜,用來(lái)解綁當(dāng)前傳入的函數(shù),同時(shí)在 subscribe 函數(shù)定義了一個(gè) isSubscribed 標(biāo)志變量來(lái)判斷當(dāng)前的訂閱是否已經(jīng)被解綁劲厌,解綁的操作就是從 nextListeners 列表中刪除當(dāng)前的監(jiān)聽(tīng)函數(shù)膛薛。

dispatch

dispatch是redux中一個(gè)非常核心的方法,也是我們?cè)谌粘i_(kāi)發(fā)中最常用的方法之一补鼻。dispatch函數(shù)是用來(lái)觸發(fā)狀態(tài)改變的哄啄,他接受一個(gè) action 對(duì)象作為參數(shù),然后 reducer 就可以根據(jù) action 的屬性以及當(dāng)前 store 的狀態(tài)风范,來(lái)生成一個(gè)新的狀態(tài)咨跌,從而改變 store 的狀態(tài);

function dispatch(action) {
    // action 必須是一個(gè)對(duì)象
    if (!isPlainObject(action)) {
      throw new Error(
        ......
      )
    }
    // type必須要有屬性硼婿,不能是undefined
    if (typeof action.type === 'undefined') {
      throw new Error(
        ......
      )
    }
    // 禁止在reducers中進(jìn)行dispatch锌半,因?yàn)檫@樣做可能導(dǎo)致分發(fā)死循環(huán),同時(shí)也增加了數(shù)據(jù)流動(dòng)的復(fù)雜度
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
//       當(dāng)前的狀態(tài)和 action 傳給當(dāng)前的reducer加酵,用于生成最新的 state
      currentState = currentReducer(currentState, action)
    } finally {  
      // 派發(fā)完畢
      isDispatching = false
    }
    // 將nextListeners交給listeners
    const listeners = (currentListeners = nextListeners)
    // 在得到新的狀態(tài)后拳喻,依次調(diào)用所有的監(jiān)聽(tīng)器,通知狀態(tài)的變更
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
    return action
  }

其中 currentState = currentReducer(currentState, action);這里的 currentReducer 是一個(gè)函數(shù)猪腕,他接受兩個(gè)參數(shù):

  • 當(dāng)前狀態(tài)
  • action

然后返回計(jì)算出來(lái)的新的狀態(tài)冗澈。

2.3 compose.js

compose 可以接受一組函數(shù)參數(shù),從右到左來(lái)組合多個(gè)函數(shù)陋葡,然后返回一個(gè)組合函數(shù)亚亲。它的源碼并不長(zhǎng),但設(shè)計(jì)的十分巧妙:


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函數(shù)的作用其實(shí)其源碼的注釋里講的很清楚了腐缤,比如下面這樣:

compose(funcA, funcB, funcC)

其實(shí)它與這樣是等價(jià)的:

compose(funcA(funcB(funcC())))

ompose 做的只是讓我們?cè)趯?xiě)深度嵌套的函數(shù)時(shí)捌归,避免了代碼的向右偏移。

2.4 applyMiddleware

applyMiddleware也是redux中非常重要的一個(gè)函數(shù)岭粤,設(shè)計(jì)的也非常巧妙惜索,讓人嘆為觀止。

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    // 利用傳入的createStore和reducer和創(chuàng)建一個(gè)store
    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.`
      )
    }
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    // 讓每個(gè) middleware 帶著 middlewareAPI 這個(gè)參數(shù)分別執(zhí)行一遍
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
  }
}

通過(guò)上面的代碼剃浇,我們可以看出 applyMiddleware 是個(gè)三級(jí)柯里化的函數(shù)巾兆。它將陸續(xù)的獲得三個(gè)參數(shù):第一個(gè)是 middlewares 數(shù)組,第二個(gè)是 Redux 原生的 createStore虎囚,最后一個(gè)是 reducer角塑,也就是上面的...args;

applyMiddleware 利用 createStore 和 reducer 創(chuàng)建了一個(gè) store淘讥,然后 store 的 getState 方法和 dispatch 方法又分別被直接和間接地賦值給 middlewareAPI 變量圃伶。

其中這一句我感覺(jué)是最核心的:

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

我特意將compose與applyMiddleware放在一塊,就是為了解釋這段代碼。因此上面那段核心代碼中窒朋,本質(zhì)上就是這樣的(假設(shè)...chain有三個(gè)函數(shù)):

dispatch = f1(f2(f3(store.dispatch))))

2.5 combineReducers

combineReducers 這個(gè)輔助函數(shù)的作用就是搀罢,將一個(gè)由多個(gè)不同 reducer 函數(shù)作為 value 的 object合并成一個(gè)最終的 reducer 函數(shù),然后我們就可以對(duì)這個(gè) reducer 調(diào)用 createStore 方法了炼邀。這在createStore的源碼的注釋中也有提到過(guò)魄揉。

并且合并后的 reducer 可以調(diào)用各個(gè)子 reducer,并把它們返回的結(jié)果合并成一個(gè) state 對(duì)象拭宁。 由 combineReducers() 返回的 state 對(duì)象洛退,會(huì)將傳入的每個(gè) reducer 返回的 state 按其傳遞給 combineReducers() 時(shí)對(duì)應(yīng)的 key 進(jìn)行命名。

下面我們來(lái)看源碼杰标,下面的源碼刪除了一些的檢查判斷兵怯,只保留最主要的源碼:

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]
  const finalReducerKeys = Object.keys(finalReducers)

// 返回最終生成的 reducer
  return function combination(state = {}, action) {
    let hasChanged = false
    //定義新的nextState
    const nextState = {}
    // 1,遍歷reducers對(duì)象中的有效key腔剂,
    // 2媒区,執(zhí)行該key對(duì)應(yīng)的value函數(shù),即子reducer函數(shù)掸犬,并得到對(duì)應(yīng)的state對(duì)象
    // 3袜漩,將新的子state掛到新的nextState對(duì)象上,而key不變
    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)
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
     // 遍歷一遍看是否發(fā)生改變湾碎,發(fā)生改變了返回新的state宙攻,否則返回原先的state
    return hasChanged ? nextState : state
  }
}

2.6 bindActionCreators

bindActionCreators可以把一個(gè) value 為不同 action creator 的對(duì)象,轉(zhuǎn)成擁有同名 key 的對(duì)象介褥。同時(shí)使用 dispatch 對(duì)每個(gè) action creator 進(jìn)行包裝座掘,以便可以直接調(diào)用它們。
bindActionCreators函數(shù)并不常用(反正我還沒(méi)有怎么用過(guò))柔滔,惟一會(huì)使用到 bindActionCreators 的場(chǎng)景就是我們需要把 action creator 往下傳到一個(gè)組件上溢陪,卻不想讓這個(gè)組件覺(jué)察到 Redux 的存在,并且不希望把 dispatch 或 Redux store 傳給它睛廊。

// 核心代碼形真,并通過(guò)apply將this綁定起來(lái)
function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
} 
// 這個(gè)函數(shù)只是把a(bǔ)ctionCreators這個(gè)對(duì)象里面包含的每一個(gè)actionCreator按照原來(lái)的key的方式全部都封裝了一遍,核心代碼還是上面的
export default function bindActionCreators(actionCreators, dispatch) {
  // 如果actionCreators是一個(gè)函數(shù)超全,則說(shuō)明只有一個(gè)actionCreator咆霜,就直接調(diào)用bindActionCreator
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }
  // 如果是actionCreator是對(duì)象或者null的話,就會(huì)報(bào)錯(cuò)
  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error(
    ... ... 
  }
 // 遍歷對(duì)象卵迂,然后對(duì)每個(gè)遍歷項(xiàng)的 actionCreator 生成函數(shù)裕便,將函數(shù)按照原來(lái)的 key 值放到一個(gè)對(duì)象中绒净,最后返回這個(gè)對(duì)象
  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
}

小節(jié)

看一遍redux见咒,感覺(jué)設(shè)計(jì)十分巧秒,不愧是大佬的作品挂疆。這次看代碼只是初看改览,往后隨著自己學(xué)習(xí)的不斷深入下翎,還需多加研究,絕對(duì)還能得到更多的體會(huì)宝当。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末视事,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子庆揩,更是在濱河造成了極大的恐慌俐东,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,826評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件订晌,死亡現(xiàn)場(chǎng)離奇詭異虏辫,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)锈拨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)砌庄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人奕枢,你說(shuō)我怎么就攤上這事娄昆。” “怎么了缝彬?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,234評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵萌焰,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我跌造,道長(zhǎng)杆怕,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,562評(píng)論 1 293
  • 正文 為了忘掉前任壳贪,我火速辦了婚禮陵珍,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘违施。我一直安慰自己互纯,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布磕蒲。 她就那樣靜靜地躺著留潦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪辣往。 梳的紋絲不亂的頭發(fā)上兔院,一...
    開(kāi)封第一講書(shū)人閱讀 51,482評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音站削,去河邊找鬼坊萝。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的十偶。 我是一名探鬼主播菩鲜,決...
    沈念sama閱讀 40,271評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼惦积!你這毒婦竟也來(lái)了接校?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,166評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤狮崩,失蹤者是張志新(化名)和其女友劉穎蛛勉,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體睦柴,經(jīng)...
    沈念sama閱讀 45,608評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡董习,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了爱只。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片皿淋。...
    茶點(diǎn)故事閱讀 39,926評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖恬试,靈堂內(nèi)的尸體忽然破棺而出窝趣,到底是詐尸還是另有隱情,我是刑警寧澤训柴,帶...
    沈念sama閱讀 35,644評(píng)論 5 346
  • 正文 年R本政府宣布哑舒,位于F島的核電站,受9級(jí)特大地震影響幻馁,放射性物質(zhì)發(fā)生泄漏洗鸵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評(píng)論 3 329
  • 文/蒙蒙 一仗嗦、第九天 我趴在偏房一處隱蔽的房頂上張望膘滨。 院中可真熱鬧,春花似錦稀拐、人聲如沸火邓。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,866評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)铲咨。三九已至,卻和暖如春蜓洪,著一層夾襖步出監(jiān)牢的瞬間纤勒,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,991評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工隆檀, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留摇天,地道東北人北滥。 一個(gè)月前我還...
    沈念sama閱讀 48,063評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像闸翅,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子菊霜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容