Redux源碼分析

Redux使用中的幾個點:javascript

  1. Redux三大設計原則
  2. Create Store
  3. Redux middleware
  4. combineReducer
  5. Provider與Connect
  6. Redux流程梳理
  7. Redux設計特色

1. Redux三大設計原則

1. 單一數據源

在傳統的 MVC 架構中,咱們能夠根據須要建立無數個 Model,而 Model 之間能夠互相監聽、觸發事件甚至循環或嵌套觸發事件,這些在 Redux 中都是不容許的。由於在 Redux 的思想裏,一個應用永遠只有惟一的數據源。
實際上,使用單一數據源的好處在於整個應用狀態都保存在一個對象中,這樣咱們隨時能夠提取出整個應用的狀態進行持久化(好比實現一個針對整個應用的即時保存功能)。此外,這樣的設計也爲服務端渲染提供了可能。java

2. 狀態是隻讀的

在 Redux 中,咱們並不會本身用代碼來定義一個 store。取而代之的是,咱們定義一個 reducer,它的功能是根據當前觸發的 action 對當前應用的狀態(state)進行迭代,這裏咱們並無直接修改應用的狀態,而是返回了一份全新的狀態。react

Redux 提供的 createStore 方法會根據 reducer 生成 store。最後,咱們能夠利用 store. dispatch
方法來達到修改狀態的目的。編程

3.狀態修改均由純函數完成

在 Redux 裏,咱們經過定義 reducer 來肯定狀態的修改,而每個 reducer 都是純函數,這意味着它沒有反作用,即接受必定的輸入,一定會獲得必定的輸出。redux

這樣設計的好處不只在於 reducer 裏對狀態的修改變得簡單、純粹、可測試,更有意思的是,Redux 利用每次新返回的狀態生成酷炫的時間旅行(time travel)調試方式,讓跟蹤每一次由於觸發 action 而改變狀態的結果成爲了可能。api

2.Create Store

咱們從store的誕生開始提及。create store函數API文檔以下:數組

createStore(reducer, [initialState], enhancer)

能夠看出,它接受三個參數:reducer、initialState 和 enhancer 。Store enhancer 是一個組合 store creator 的高階函數,返回一個新的強化過的 store creator。這與 middleware 類似,它也容許你經過複合函數改變 store 接口。緩存

再來看看他的返回值:閉包

{
    dispatch: f (action),
    getState: f (),
    replaceReducer: f (nextReducer),
    subscribe: f (listener),
    Symbol(observable): f ()    
}

store的返回值就是一個普通對象,裏面有幾個經常使用的方法:架構

  • dispatch:就是咱們最經常使用的dispatch方法,派發action。
  • getState:經過該方法,咱們能夠拿到當前狀態樹state。
  • replaceReducer:這個方法主要用於 reducer 的熱替換,下面介紹該方法。
  • subscribe:添加一個變化監聽器。每當 dispatch(action)的時候就會執行,state 樹中的一部分可能已經變化。
  • observable:觀察者模式,用於處理訂閱關係。

這裏挑幾個方法介紹:

getState

在完成基本的參數校驗以後,在 createStore 中聲明以下變量及 getState 方法:

var currentReducer = reducer
var currentState = initialState
var listeners = [] // 當前監聽 store 變化的監聽器
var isDispatching = false // 某個 action 是否處於分發的處理過程當中
/**
* Reads the state tree managed by the store.
 *
* @returns {any} The current state tree of your application.
 */
function getState() {
 return currentState
}

getState方法就是簡單返回當前state,若是state沒有被reducer處理過,他就是initialState。

subscribe

在 getState 以後,定義了 store 的另外一個方法 subscribe:

function subscribe(listener) {
 listeners.push(listener)
 var isSubscribed = true
 return function unsubscribe() {
 if (!isSubscribed) {
 return
 }
 isSubscribed = false
 var index = listeners.indexOf(listener)
 listeners.splice(index, 1)
 }
}

Store 容許使用store.subscribe方法設置監聽函數,一旦 State 發生變化,就自動執行這個函數。

顯然,只要把 View 的更新函數(對於 React 項目,就是組件的render方法或setState方法)放入listen,就會實現 View 的自動渲染。你可能會感到奇怪,好像咱們在 Redux 應用中並無使用 store.subscribe 方法?事實上,

React Redux 中的 connect 方法隱式地幫咱們完成了這個工做。

store.subscribe方法返回一個函數,調用這個函數就能夠解除監聽。

dispatch

dispatch是redux的核心方法:

function dispatch(action) {
    if (!isPlainObject(action)) {
        throw new Error(
            'Actions must be plain objects. ' +
            'Use custom middleware for async actions.'
        )
    }
    if (typeof action.type === 'undefined') {
        throw new Error(
            'Actions may not have an undefined "type" property. ' +
            'Have you misspelled a constant?'
            )
        }
    if (isDispatching) {
        throw new Error('Reducers may not dispatch actions.')
    }
    try {
        isDispatching = true
        currentState = currentReducer(currentState, action)
    } finally {
        isDispatching = false
    }
    listeners.slice().forEach(listener => listener())
    return action
}

判斷當前是否處於某個 action 的分發過程當中,這個檢查主要是爲了不在 reducer 中分發 action 的狀況,由於這樣作可能致使分發死循環,同時也增長了數據流動的複雜度。

確認當前不屬於分發過程當中後,先設定標誌位,而後將當前的狀態和 action 傳給當前的reducer,用於生成最新的 state。這看起來一點都不復雜,這也是咱們反覆強調的 reducer 工做過程——純函數、接受狀態和 action 做爲參數,返回一個新的狀態。

在獲得新的狀態後,依次調用全部的監聽器,通知狀態的變動。須要注意的是,咱們在通知監聽器變動發生時,並無將最新的狀態做爲參數傳遞給這些監聽器。這是由於在監聽器中,咱們能夠直接調用 store.getState() 方法拿到最新的狀態。

最終,處理以後的 action 會被 dispatch 方法返回。

replaceReducer

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

    currentReducer = nextReducer;
    dispatch({ type: ActionTypes.INIT });
  }

這是爲了拿到全部 reducer 中的初始狀態(你是否還記得在定義 reducer 時,第一個參數爲previousState,若是該參數爲空,咱們提供默認的 initialState)。只有全部的初始狀態都成功獲取後,Redux 應用纔能有條不紊地開始運做。

3.Redux middleware

It provides a third-party extension point between dispatching an action, and the moment it reaches
the reducer

它提供了一個分類處理 action 的機會。在middleware 中,你能夠檢閱每個流過的 action,挑選出特定類型的action 進行相應操做,給你一次改變 action 的機會。

常規的同步數據流模式的流程圖以下:
clipboard.png
不一樣業務需求下,好比執行action以前和以後都要打log;action觸發一個異步的請求,請求回來以後渲染view等。須要爲這一類的action添加公共的方法或者處理,使用redux middleware流程圖以下:
clipboard.png
每個 middleware 處理一個相對獨立的業務需求,經過串聯不一樣的 middleware 實現變化多樣的功能。好比上面的業務,咱們把處理log的代碼封裝成一個middleware,處理異步的也是一個middleware,二者串聯,卻又相互獨立。

使用middleware以後,action觸發的dispatch並非原來的dispatch,而是通過封裝的new dispatch,在這個new dispatch中,按照順序依次執行每一個middleware,最後調用原生的dispatch。

咱們來看下logger middleware如何實現的:

export default store => next => action => {
    console.log('dispatch:', action); 
    next(action);
    console.log('finish:', action);
 }

這裏代碼十分簡潔,就是在next調用下一個middleware以前和以後,分別打印兩次。

Redux 提供了 applyMiddleware 方法來加載 middleware,該方法的源碼以下:

import compose from './compose';

export default function applyMiddleware(...middlewares) {
    return function (next) {
        return function (reducer, initialState) {
            let store = next(reducer, initialState);
            let dispatch = store.dispatch;
            let chain = [];
            var middlewareAPI = {
                getState: store.getState,
                dispatch: (action) => dispatch(action),
            };
            chain = middlewares.map(middleware => middleware(middlewareAPI));
            dispatch = compose(...chain)(store.dispatch);

            return {
                ...store,
                dispatch,
            };
        }
    }
}

其中compose源碼以下:

function compose(...funcs) {
    return arg => funcs.reduceRight((composed, f) => f(composed), arg);
}

使用的時候,以下:

const newStore = applyMiddleware([mid1, mid2, mid3, ...])(createStore)(reducer, initialState);

ok,相關源碼已就位,咱們來詳細解析一波。

函數式編程思想設計 :middleware 的設計有點特殊,是一個層層包裹的匿名函數,這實際上是函數式編程中的
currying,它是一種使用匿名單參數函數來實現多參數函數的方法。applyMiddleware 會對 logger 這個middleware 進行層層調用,動態地將 store 和 next 參數賦值。currying 的 middleware 結構的好處主要有如下兩點。

  • 易串聯:currying 函數具備延遲執行的特性,經過不斷 currying 造成的 middleware 能夠累積參數,再配合組合(compose)的方式,很容易造成 pipeline 來處理數據流。
  •  共享 store: 在 applyMiddleware 執行的過程當中,store 仍是舊的,可是由於閉包的存在,applyMiddleware 完成後,全部的 middleware 內部拿到的 store 是最新且相同的。

給 middleware 分發 store:newStore建立完成以後,applyMiddleware 方法陸續得到了3個參數,第一個是 middlewares 數組[mid1, mid2, mid3, ...],第二個是 Redux 原生的 createStore ,最後一個是 reducer。而後,咱們能夠看到 applyMiddleware 利用 createStore 和 reducer 建立了一個 store。而 store 的 getState方法和 dispatch 方法又分別被直接和間接地賦值給 middlewareAPI 變量 store:

const middlewareAPI = {
 getState: store.getState,
 dispatch: (action) => dispatch(action),
};
chain = middlewares.map(middleware => middleware(middlewareAPI));

而後,讓每一個 middleware 帶着 middlewareAPI 這個參數分別執行一遍。執行完後,得到 chain數組 [f1, f2, ... , fx, ..., fn],它保存的對象是第二個箭頭函數返回的匿名函數。由於是閉包,每一個匿名函數均可以訪問相同的 store,即 middlewareAPI。

middlewareAPI 中的 dispatch 爲何要用匿名函數包裹呢?

咱們用 applyMiddleware 是爲了改造 dispatch,因此 applyMiddleware 執行完後,dispatch 是變化了的,而 middlewareAPI 是 applyMiddleware 執行中分發到各個 middleware 的,因此必須用匿名函數包裹 dispatch,這樣只要 dispatch 更新了,middlewareAPI 中的 dispatch 應用也會發生變化。

組合串聯 middleware:這一層只有一行代碼,倒是 applyMiddleware 精華之所在dispatch = compose(...chain)(store.dispatch); ,其中 compose 是函數式編程中的組合,它將 chain 中的全部匿名函數 [f1, f2, ... , fx, ..., fn]組裝成一個新的函數,即新的 dispatch。當新 dispatch 執行時,[f1, f2, ... , fx, ..., fn],從右到左依次執行。

compose(...funcs) 返回的是一個匿名函數,其中 funcs 就是 chain 數組。當調用 reduceRight時,依次從 funcs 數組的右端取一個函數 fx 拿來執行,fx 的參數 composed 就是前一次 fx+1 執行的結果,而第一次執行的 fn(n 表明 chain 的長度)的參數 arg 就是 store.dispatch。因此,當 compose 執行完後,咱們獲得的 dispatch 是這樣的,假設 n = 3:

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

這時調用新 dispatch,每個 middleware 就依次執行了。

在 middleware 中調用 dispatch 會發生什麼:通過 compose 後,全部的 middleware 算是串聯起來了。但是還有一個問題,在分發 store 時,咱們提到過每一個 middleware 均可以訪問 store,即 middlewareAPI 這個變量,也能夠拿到 store 的dispatch 屬性。那麼,在 middleware 中調用 store.dispatch() 會發生什麼,和調用 next() 有區別嗎?如今咱們來講明二者的不一樣:

const logger = store => next => action => {
 console.log('dispatch:', action);
 next(action);
 console.log('finish:', action);
};
const logger = store => next => action => {
 console.log('dispatch:', action);
 store.dispatch(action);
 console.log('finish:', action);
};

在分發 store 時咱們解釋過,middleware 中 store 的 dispatch 經過匿名函數的方式和最終compose 結束後的新 dispatch 保持一致,因此,在 middleware 中調用 store.dispatch() 和在其餘任何地方調用的效果同樣。而在 middleware 中調用 next(),效果是進入下一個 middleware,下圖就是redux middleware最著名的洋蔥模型圖。
clipboard.png

4.combineReducer

若是一個項目過大,咱們一般按模塊來寫reducer,可是redux create store只接受一個reducer參數,因此咱們須要合併reducer。這裏就用到了redux提供的combineReducer輔助函數:

combineReducers({
      layout,
      home,
      ...asyncReducers
  })

這個函數用起來很簡單,就是傳入一個對象,key是模塊reducer對應的名字, 值是對應reducer。值是一個function,至關因而一個新的reducer,源碼以下:

export default function combineReducers(reducers) {
  var reducerKeys = Object.keys(reducers)
  var finalReducers = {}
  for (var i = 0; i < reducerKeys.length; i++) {
    var key = reducerKeys[i]

    if (process.env.NODE_ENV !== 'production') {
      if (typeof reducers[key] === 'undefined') {
        warning(`No reducer provided for key "${key}"`)
      }
    }

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

  if (process.env.NODE_ENV !== 'production') {
    var unexpectedKeyCache = {}
  }

  var sanityError
  try {
    assertReducerSanity(finalReducers)
  } catch (e) {
    sanityError = e
  }

  return function combination(state = {}, action) {
    if (sanityError) {
      throw sanityError
    }

    if (process.env.NODE_ENV !== 'production') {
      var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache)
      if (warningMessage) {
        warning(warningMessage)
      }
    }

    var hasChanged = false
    var nextState = {}
    for (var i = 0; i < finalReducerKeys.length; i++) {
      var key = finalReducerKeys[i]
      var reducer = finalReducers[key]
      var previousStateForKey = state[key]
      var nextStateForKey = reducer(previousStateForKey, action)
      if (typeof nextStateForKey === 'undefined') {
        var errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
  }
}

源碼不是不少,除去一些驗證代碼,剩下的就是說:return一個function,咱們暫時稱呼他combination,就至關因而與一個總的reducer,每次action都會走到combination中,combination會遍歷輸入的reducer,將action放到每一個reducer中執行一下,計算出返回結果就是nextState,nextState於previousState若是!==說明改變了,返回nextState,不然返回執行以前的state。

這也解釋了不一樣模塊actionType若是相同的話,兩個模塊的reducer都會走一遍的問題,在actionType名稱前面加上模塊前綴便可解決問題。

5. Provider與Connect

Provider與Connet組件都是React-Redux提供的核心組件,二者看起來功能同樣,都是幫助容器組件獲取store中的數據,可是原理與功能卻不一樣。

Provider

Provider組件在全部組件的最外層,其接受store做爲參數,將store裏的state使用context屬性向下傳遞。部分源碼:

export default class Provider extends Component {
 getChildContext() {
 return { store: this.store }
 }
 constructor(props, context) {
 super(props, context)
 this.store = props.store
 }
 render() {
 const { children } = this.props
 return Children.only(children)
 }
}

利用context這個屬性,Provider全部子組件都可以拿到這個屬性。

Connect

connect實現的功能是將須要關聯store的組件和store的dispatch等數據混合到一塊,這塊就是一個高階組件典型的應用:

import hoistStatics from 'hoist-non-react-statics'
export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
 // ...
 return function wrapWithConnect(WrappedComponent) {
 // ...
 class Connect extends Component {
 // ...
 render() {
 // ...
 if (withRef) {
 this.renderedElement = createElement(WrappedComponent, {
 ...this.mergedProps,
 ref: 'wrappedInstance'
 })
 } else {
 this.renderedElement = createElement(WrappedComponent,
 this.mergedProps
 )
 }
 return this.renderedElement
 }
 }
 // ...
 return hoistStatcis(Connect, WrappedComponent);
 }
}

仍是先從他的四個參數提及:

1.mapStateToProps

connect 的第一個參數定義了咱們須要從 Redux 狀態樹中提取哪些部分看成 props 傳給當前組件。通常來講,這也是咱們使用 connect 時常常傳入的參數。事實上,若是不傳入這個參數,React 組件將永遠不會和 Redux 的狀態樹產生任何關係。具體在源代碼中的表現爲:

export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
 const shouldSubscribe = Boolean(mapStateToProps)
 // ...
 class Connect extends Component {
 // ...
 trySubscribe() {
 if (shouldSubscribe && !this.unsubscribe) {
 this.unsubscribe = this.store.subscribe(this.handleChange.bind(this))
 this.handleChange()
 }
 }
 // ...
 }
}

mapStateToProps會訂閱 Store,每當state更新的時候,就會自動執行,從新計算 UI 組件的參數,從而觸發 UI 組件的從新渲染。

mapStateToProps的第一個參數老是state對象,還可使用第二個參數,表明容器組件的props對象。

這塊的源碼相對較簡單:

const mapState = mapStateToProps || defaultMapStateToProps 
class Connect extends Component { 
    computeStateProps(store, props) {
        if (!this.finalMapStateToProps) {
          return this.configureFinalMapState(store, props)
        }

        const state = store.getState()
        const stateProps = this.doStatePropsDependOnOwnProps ?
          this.finalMapStateToProps(state, props) :
          this.finalMapStateToProps(state)

        if (process.env.NODE_ENV !== 'production') {
          checkStateShape(stateProps, 'mapStateToProps')
        }
        return stateProps
      }

      configureFinalMapState(store, props) {
        const mappedState = mapState(store.getState(), props)
        const isFactory = typeof mappedState === 'function'

        this.finalMapStateToProps = isFactory ? mappedState : mapState
        this.doStatePropsDependOnOwnProps = this.finalMapStateToProps.length !== 1

        if (isFactory) {
          return this.computeStateProps(store, props)
        }

        if (process.env.NODE_ENV !== 'production') {
          checkStateShape(mappedState, 'mapStateToProps')
        }
        return mappedState
      }
}

這塊原理很簡單,進行一些參數校驗,判斷第一個參數mapStateToProps返回值是否爲function,若是是遞歸調用,不是的話算出返回值。若是沒傳這個參數,默認給{}。

咱們可能會疑惑爲何傳給 connect 的第一個參數自己是一個函數,react-redux 還容許這個函數的返回值也是一個函數呢?
簡單地說,這樣設計能夠容許咱們在 connect 的第一個參數裏利用函數閉包進行一些複雜計算的緩存,從而實現效率優化的目的

當咱們使用的時候:

const mapStateToProps = (state, props) => ({
    home: state.home,
    layout: state.layout
});

使用ownProps做爲參數後,若是容器組件的參數發生變化,也會引起 UI 組件從新渲染

2.mapDispatchToProps

人如其名,它接受 store 的 dispatch 做爲第一個參數,同時接受 this.props 做爲可選的第二個參數。利用這個方法,咱們能夠在 connect 中方便地將 actionCreator 與 dispatch 綁定在一塊兒(利用 bindActionCreators 方法),最終綁定好的方法也會做爲 props 傳給當前組件。這塊的源碼與mapStateToProps同樣,就不貼了。

bindActionCreator

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

3.mergeProps

前兩個參數返回的對象,都要跟組件自身的props merge一下,造成一個新的對象賦值給對應組件,咱們能夠在這一步作一些處理,這個參數就是幹這個的,該參數簽名:

mergeProps(stateProps, dispatchProps, ownProps): props

默認狀況若是沒傳該參數,返回Object.assign(ownProps, stateProps, dispatchProps)

4.options

若是指定這個參數,能夠定製 connector 的行爲。

  • [pure = true] (Boolean): 若是爲 true,connector 將執行 shouldComponentUpdate 而且淺對比 mergeProps 的結果,避免沒必要要的更新,前提是當前組件是一個「純」組件,它不依賴於任何的輸入或 state 而只依賴於 props 和 Redux store 的 state。默認值爲 true。
  • [withRef = false] (Boolean): 若是爲 true,connector 會保存一個對被包裝組件實例的引用,該引用經過 getWrappedInstance() 方法得到。默認值爲 false。

這個connect組件還幹了一件事,狀態緩存判斷。當store變了的時候,先後狀態判斷,若是狀態不等,更新組件,而且完成事件分發。

6. Redux流程梳理

上面講了大量的函數源碼,這麼些函數之間的關係:
clipboard.png
初始化階段:

  1. createStore建立一個store對象
  2. 將store對象經過參數給Provider組件
  3. Provider組件將store經過context向子組件傳遞
  4. Connect組件經過context獲取到store,存入本身的state
  5. componentDidMount裏面訂閱store.subscribe事件

更新數據階段:

  1. 用戶事件觸發
  2. actionCreator生成action交給dispatch
  3. 實際上交給了封裝後的中間層(compose(applyMiddleware(...)))
  4. 請求依次經過每一箇中間件,中間件經過next進行下一步
  5. 最後一箇中間件將action交給store.dispatch
  6. dispatch內部將action交給reducer執行
  7. combineReducer將每一個子reducer執行一遍算出新的state
  8. dispatch內部調用全部訂閱事件
  9. Connect組件handleChange事件觸發判斷新state和舊state是否===
  10. 而且判斷新的state是否與mapStateToProps shallowEqual
  11. 不等則setState觸發更新

7.Redux設計技巧

  1. 匿名函數&&閉包使用

    redux核心函數大量使用了匿名函數和閉包來實現數據共享和狀態同步。

  2. 函數柯里化使用

    使用函數柯里化s實現參數複用,本質上是下降通用性,提升適用性。

  3. 核心狀態讀取是拷貝而不是地址

    對於state這種核心狀態使用getState()計算出新的state,而不是直接返回一個state對象。

  4. 觀察者訂閱者是核心實現

    使用觀察者訂閱者模式實現數據響應。

  5. context這個api的使用

    平時開發不常接觸的api實現Provider與Connect通訊。

相關文章
相關標籤/搜索