redux源碼解讀

  react在作大型項目的時候,前端的數據通常會愈來愈複雜,狀態的變化難以跟蹤、沒法預測,而redux能夠很好的結合react使用,保證數據的單向流動,能夠很好的管理整個項目的狀態,可是具體來講,下面是redux的一個核心流程圖:前端

        

  即整個項目的數據存儲在Store中,每一個狀態下Store會生成一個state,一個state對應着一個view,而用戶只能接觸到view,不能接觸到store,那咱們怎麼才能讓store中的數據發生改變呢? 因此,必需要經過view來間接改變,即用戶點擊,產生action,不能點擊一次,建立一個action,因此須要一個action creator,而後將這個action經過dispatch函數送到store中,這個過程當中,可使用一些中間件來處理一些異步操做,而後將數據交給store,store拿到數據以後,經過reducer來根據不一樣的action的type來處理數據,返回一個新的state,經過新的state,就能夠再產生一個新的view了。 而且能夠看到 store、view、action這樣的一個單項數據流。 react

    

  爲了更好地理解redux,咱們能夠讀一下redux的源碼。 webpack

 

  首先,咱們將redux源碼獲得,總體目錄以下:git

        

   而redux源碼的核心固然是處在src中的,dist、es、lib都不是最重要的,因此,咱們展開src,能夠看到下面的目錄:github

        

  下面主要說一下總體:web

  • utils下的warning.js文件用於控制檯錯誤日誌輸出,能夠忽略。
  • index.js 爲入口文件,看源碼時應該首先看這個文件。
  • createStore是用於建立store的,因此是主流程,也是比較重要的一環。 
  • 其餘四個文件 --- applyMiddlewares.js、 bindActionCreators.js、combineReducers.js、compose.js這四個文件屬於助攻型文件。

 

入口文件 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'

/*
* This is a dummy function to check if the function name has been altered by minification.
* If the function has been minified and NODE_ENV !== 'production', warn the user.
*/
function isCrushed() {}

if (
  process.env.NODE_ENV !== 'production' &&
  typeof isCrushed.name === 'string' &&
  isCrushed.name !== 'isCrushed'
) {
  warning(
    'You are currently using minified code outside of NODE_ENV === \'production\'. ' +
    'This means that you are running a slower development build of Redux. ' +
    'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' +
    'or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) ' +
    'to ensure you have the correct code for your production build.'
  )
}

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

咱們能夠看到: 在index.js中,主要是從主流程文件、幾個輔助api文件以及日誌打印文件中獲取了接口,而後中間是一些環境方面的警告,能夠忽略,最後就經過這個index文件導出了全部咱們在redux使用的過程當中所須要的api,因此入口文件index.js就是起了一個橋樑了做用,很是簡單,可是很重要。編程

 

 

主流程文件: createStore.js

  createStore.js主要是用於生成store的,咱們還能夠從最後暴露的對象看出其暴露了幾個方法:redux

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

  也就是說,這個主流程文件整個就是在定義了這麼幾個方法,下面看卡源碼(被精簡了,只留下重要部分):api

// 這個action必定是會被最早觸發的,從redux-devtools就能夠看得出來。
export const ActionTypes = {
  INIT: '@@redux/INIT'
}

// 接受三個參數
// 第一個參數是必須的,reducer,用來根據給定的狀態和action來返回下一個狀態樹,進一步致使頁面發生變化。
// 第二個參數是一個狀態,這個不是必須的,大部分時候不須要。
// 第三個參數是enhancer,一般是一箇中間件,好比使用redux-devtools這個中間件。
export default function createStore(reducer, preloadedState, enhancer) {

  // 當前的reducer  
  let currentReducer = reducer

  // 當前的狀態。
  let currentState = preloadedState

  // 能夠看出,這個是一個訂閱者,state有變化,須要告訴這些Listeners。 
  let currentListeners = []

  // 後續的listeners,是不斷更新的。
  let nextListeners = currentListeners

  // 是否dispatch。
  let isDispatching = false

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

  // 獲取當前的state樹
  function getState() {
    return currentState
  }

  /**

   * @param {Function} listener A callback to be invoked on every dispatch.
   * @returns {Function} A function to remove this change listener.
   */
  
  // 接收的是一個函數做爲參數, 這個函數會在每一次dispatch的時候被調用。
  function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected listener to be a function.')
    }

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

  // 惟一觸發state改變的方式。 dispatch a action.
  // 這裏是dispatch的一個基本的實現,只能提供一個普通的對象。 若是你但願dispatch一個Promise、thunk、obserbable等,你須要包裝你的store
  // 建立函數進入一個相應的中間件,如 redux-thunk。  
  // 這個action必定要包含type類型。最後也會返回這個action
  function dispatch(action) {

    // 若是正在dispatch,則拋出錯誤。
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}數組

// dispatch的過程當中,每個listener都會被調用。 由於在subscribe中傳入的參數就是一個listener函數。
    const listeners = currentListeners = nextListeners
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

  // 替換一個新的reducer
  function replaceReducer(nextReducer) {
    currentReducer = nextReducer
    dispatch({ type: ActionTypes.INIT })
  }

  // observable函數
  function observable() {
    const outerSubscribe = subscribe
    return {
      subscribe(observer) {
        if (typeof observer !== 'object') {
          throw new TypeError('Expected the observer to be an object.')
        }

        function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }

        observeState()
        const unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },

      [$$observable]() {
        return this
      }
    }
  }
   
  // store被建立,就會有一個INIT action被處罰,因此每一個reducer就會返回他們的初始值了。 
  dispatch({ type: ActionTypes.INIT })

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

getState方法很是簡單,就是獲取當前的store中的state,不用過多贅述。

replaceReducer方法也很簡單, 就是簡單的替換reducer。 

 

 


 

其中,subscribe用於註冊監聽事件,而後返回取消訂閱的函數,把全部的訂閱事件都放在了一個訂閱數組裏,只要維護這個數組就行了,subscribe的做用就是這麼簡單。 

每次dispatch的時候就會依次調用數組中的監聽事件。

store.subscribe()方法總結:

  • 入參函數放入監聽隊列

  • 返回取消訂閱函數

    


 

  

再看看dispatch方法,dispatch是觸發state改變的惟一方式,最爲核心的就是下面的這段代碼了:

    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    const listeners = currentListeners = nextListeners
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

這段代碼中,首先,將isDispatching設置爲了true,而後就調用currentReducer返回了一個新的currentState, 這樣就成功了改變了當前的狀態,最後, 改變了狀態以後,就開始把subscribe函數中註冊的事件開始以此執行。OK! 到這裏,dispatch方法就比較清楚了。

因此,這裏對dispatch方法作一個總結:

  • 調用reducer, 傳入參數(currentState, action)。
  • 按順序執行訂閱的listener。
  • 返回action。 

 

ok! 至此,主流程文件就已經分析完了,總體仍是比較簡單的,比較重要的一個函數就是dispatch,但也是很好理解的。

 

下面,主要講一講剩下的幾個輔助文件:

bindActionCreators.js

bindActionCreators把action creators轉成擁有同名keys的對象,使用dispatch把每一個action creator包裝起來,這樣能夠直接調用它們。

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

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

  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error(
      `bindActionCreators expected an object or a function, instead received ${actionCreators === null ? 'null' : typeof actionCreators}. ` +
      `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
    )
  }

  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
}

實際狀況用到的並很少,唯一的應用場景是當你須要把action creator往下傳到一個組件上,卻不想讓這個組件覺察到Redux的存在,並且不但願把Redux Store或dispatch傳給它。

 

 


 

CombineReducers.js

  這個文件中暴露的方法是咱們經常使用的,由於在寫reducer的時候,每每須要根據類別不一樣,寫多個reducer,可是根據createStore能夠知道,只有一個reducer能夠被傳入,因此這裏的combineReducers就是爲了將多個reducer合併成一個reducer的。具體源碼以下(通過精簡以後,就只剩下30多行了):

// 接受一個對象做爲參數,這個對象的值是須要被combine的不一樣的reducer函數。
// 返回一個函數, 這個函數就是一個大的reducer了。
export default function combineReducers(reducers) {
  // 獲取reducer的全部的keys數組。
  const reducerKeys = Object.keys(reducers)
  // 最終的reducer對象。
  const finalReducers = {}
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]
    // 將全部的reducer從新放在finalReducers中,至關於淺拷貝。
    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  // 獲取最終的全部的reducers數組。
  const finalReducerKeys = Object.keys(finalReducers)

  // 返回了一個函數,能夠看出這個函數和咱們一個一個定義的reducer函數是相似的,因此,這就是一個大的reducer函數。  
  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)
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
  }
}

因此這個combineReducers仍是很明確的,就是將全部的reducer組合成一個大的。

 


 

compose.js

  這個函數用於組合傳進來的一系列函數,在中間件的時候會用到,能夠看到,執行的最終結果就是把一系列函數串聯起來:

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

  在中間件的時候會用到這個函數

 


applyMiddleware.js

  這個函數用於 store 加強。 

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
    }
  }
}

  用法大體以下:

const store = createStore(reducer,applyMiddleware(…middlewares))
or
const store = createStore(reducer,{},applyMiddleware(…middlewares))

  

  好比一個比較經常使用的redux-thunk中間件,源碼的關鍵代碼以下:

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

        return next(action);
      };
    };
  };
}

 

 

  做用的話能夠看到,這裏有個判斷:若是當前action是個函數的話,return一個action執行,參數有dispatch和getState,不然返回給下箇中間件。這種寫法就拓展了中間件的用法,讓action能夠支持函數傳遞。即若是action是一個函數,那麼咱們就能夠進一步來處理了,若是這個action是一個對象,說明就要直接又dispatch來觸發了,即這裏的action其實是在真正的dispatch以前所作的一些工做。 

 

 


 

 

通常,咱們認爲redux屬於函數式編程,即函數是第一等公民、數據是不可變的(在reducer中,咱們但願每次返回一個新的state,而不是修改舊的state,而後返回,因此這裏強調的就是不可變的)、有肯定的輸入就有肯定的輸出。   總體來講,可能redux不是純純的函數式編程,可是也比較符合函數式編程的風格了。

以下:

const arr = [1, 2, 3];

arr.push(4); //這樣很差,看到這代碼我就方了,須要從上往下琢磨一下arr到底存的是啥
const newArr = [...arr, 4]; //這樣,arr不會被修改,很放心,要修改過的版本用newArr就行了

 

 

以下:

const me = {name: 'Morgan'};

me.skill = 'React'; //這樣很差,拿不許me裏是啥了
const newMe = {...me, skill: 'React'}; //這樣,me不會被修改
相關文章
相關標籤/搜索