redux真的不復雜——第二篇:react-redux源碼分析

第一篇連接: redux真的不復雜——源碼解讀javascript

預備知識:1. 瞭解redux的基本使用; 2. Context APIhtml

瞭解redux原理更好,若是不瞭解,不妨先看看第一篇博客。java

redux是一個狀態管理的工具,本質上是一個js對象(包含狀態,以及一些處理狀態的方法)。react

因此redux具備很強的適應性,能夠配合其餘工具/框架一塊兒使用。git

react-redux則是一個讓你更容易地在react中使用redux的工具。github

爲何須要redux

咱們使用redux的目的是存儲狀態,在react中有存儲狀態的東西嗎?redux

有,state。可是state有一些侷限性。api

對於一個組件來講,(用setState觸發)state的變化會觸發這個組件以及它全部子組件的更新,因此爲了優化考慮,咱們每每將state放在更「局部」的組件中,這樣state的變化只會引發最少的(有必要的)更新。緩存

那麼若是咱們的react應用有一些狀態在多個地方均可能用到,特別是一些全局數據(好比當前用戶信息,全局的通知等等),對於這些數據咱們有兩個選擇:app

  1. 在各個局部組件中各存一份

    優勢是:保證了數據的變化只會引發最小的組件更新,

    缺點是

    • 難以保證各處的數據同步
    • 可能各處會重複請求相同的API,損失了必定性能
  2. 在全局組件中存一份

    優勢是:只須要在一個地方請求API,數據是徹底同步的

    缺點是:數據的變化會引發整個應用大量的更新。

你會發現這兩個選擇各有優缺點,但仔細想一想,你會發現其實咱們有第三個選擇:

利用Context API。在全局組件外包一個Provider,將數據存在Provider上。子組件經過訪問context來使用這些數據

這樣就兼顧了各個優勢:

  • 數據易同步
  • 只需在一個地方請求API
  • 全局數據的更新不會引發組件的更新(Context api的特性)

一切彷彿變得美好了,不是嗎?

可是問題又來了:

  • 全局(Provider上)的數據你如何管理(特別是當數據的結構複雜了以後)?
  • 子組件如何根據須要更新這些數據?

這個時候你就會想到redux了,redux提供了一套優雅的管理狀態的方案。

優雅在什麼地方?接着往下看。

爲何須要react-redux

想要使用context api的方案,而且還要使用redux,那麼你須要作的事情有:

  1. 用redux建立一個store
  2. 在react應用的最外層包一個Provider,store放在Provider上。
  3. 子組件想要獲取數據時:在組件外包一個Consumer,Consumer獲取到store,傳遞給組件。
  4. 當子組件想要更新數據時:調用store的dispatch方法觸發store的更新。

差很少就這些,是否是也挺簡單?

可是從技術上來說,你須要作的事情有:

  1. 本身寫一個provider
  2. 每次想要使用provider的數據的時候,本身寫一個Consumer組件
  3. 每次想要更新數據的時候,本身調用store的diapatch方法。

難受嗎?

你須要react-redux,它幫你把這些操做都封裝了起來。

react-redux源碼分析

爲了源碼更清晰,分析時只展現了一些核心代碼,省略了錯誤處理,通用性處理等代碼。建議你參照着真正的源碼閱讀。

react-redux的使用

在看源碼以前,先簡單回憶一下react-redux的用法:

  1. 從react-redux庫引入一個Provider組件,將redux建立的store做爲屬性傳遞給這個Provider組件。
  2. 給connect傳遞mapStateToPropsmapDispatchToProps兩個參數(還有其餘可選參數mergeProps,options),獲得一個高階組件【注1】。
  3. 用高階組件「包裝」咱們本身的組件,就能在本身的組件中獲得對應的props。

【注1】高階組件:輸入爲組件,輸出爲另外一個組件的函數

ok,咱們來看看源碼的結構:

還挺複雜,不過不要緊,看看index.js:

//index.js

import Provider, { createProvider } from './components/Provider'
import connectAdvanced from './components/connectAdvanced'
import connect from './connect/connect'

export { Provider, createProvider, connectAdvanced, connect }
複製代碼

createProvider和connectAdvanced這兩個方法是定製react-redux的工做方式時使用的,咱們通常不會直接用到,因此咱們就從Provider和connect這兩個模塊入手。

Provider

Provider.js的源碼比較少,結構也很簡單:

//Provider.js

//import略
//輸出建立Provider的函數
export function createProvider(storeKey = 'store'){
    class Provider extends Component {
        // ...
    }
    
    return Provider
}

//默認輸出建立後的Provider
export default createProvider()
複製代碼

這個文件提供了兩個輸出,一個是建立Provider的函數,還有一個是建立過的Provider。

下面咱們來詳細看看Provider這個組件具體是如何實現的:

class Provider extends Component {
    // 訪問context的鉤子函數【注2】
    getChildContext() {
        // 返回一個對象,鍵是store的標識(可自定義),值是store
        return { [storeKey]: this[storeKey] }
    }
    
    constructor(props, context) {
        super(props, context)
        // 將咱們(經過props)傳進來的store存在本身的實例中。
        this[storeKey] = props.store
    }
    
    render() {
        // Children.only是react提供的API函數,
        // 做用是限制this.props.children只能是一個React元素,不然會報錯
        return Children.only(this.props.children)
    }
}

Provider.childContextTypes = {
    // ...
}
複製代碼

看到這裏,能夠發現Provider的實現很是簡單,只是將咱們(經過props)傳進去的store,建立了一個context而已。

注2:React能夠經過在一個組件中設置getChildContextchildContextTypes來建立context,在其子組件中設置contextTypes屬性來接收context

原來,react-redux只是將store放到Provider組件的context上。那麼問題來了,

問題1

Provider的子組件如何使用store?


Connect

顧名思義,connect函數的做用是——」鏈接「,將一個正常的組件與咱們的store鏈接起來,這樣子組件就能夠「使用」store了。

然而其實是如何實現鏈接的呢?若是你使用過react-redux的話,你就知道是:

  1. 使用connect建立一個高階組件
  2. 而後用高階組件包裹一個組件,向組件傳遞額外的props
  3. 組件內部經過props,可以:
    • 讀取store
    • 觸發(dispatch)store中的action。

咱們使用connect建立高階組件時一般會傳入mapStateToPropsmapDispatchToProps,然而高階組件並無直接將其經過props傳進被包裹組件——而是通過篩選和包裝後,再經過props傳入一些東西(store的一部分分支,或者自動dispatch的action creator)。

回答問題1(Provider的子組件如何使用store?):

經過將「篩選和包裝」後的與store相關的東西,經過props傳入組件,來使用store

可見,篩選和包裝是一個很是重要的任務,那麼它是在connect中實現的嗎?

咱們來看看connect的源碼:

//connect.js

export function createConnect({ connectHOC = connectAdvanced, // 記住這個函數,後面會講 // 選擇器工廠:根據一些配置生成一個選擇器 // (選擇器工廠的實現,以及選擇器的做用後面咱們會講) selectorFactory = defaultSelectorFactory // ... 一些處理connect參數的工廠函數 } = {}) {
    // 這裏纔是connect函數
    return function connect({ mapStateToProps, mapDispatchToProps, mergeProps, options={} }){ 
        // connect的參數有:mapStateToProps,mapDispatchToProps,mergeProps,options
        // 然而這些參數並不能直接使用,它們多是對象,也多是函數
        // 因此在這裏進行通用性處理,是他們能夠直接使用
        
        // connect返回了一個高階組件(由connectHOC建立)
        return connectHOC(selectorFactory, { /*配置對象*/ })
    }
}

// 默認輸出的connect
export default createConnect()
複製代碼

彷佛connect並無作篩選和包裝這件事,僅僅返回了一個高階組件,而這個高階組件默認是由connectAdvanced建立的。

因此也就是說:

connect只是connectAdvanced(這個函數一會再看)的一個包裝,connect自己只是一個預處理函數,真正的「篩選和包裝」實際上是在connectAdvanced這個函數裏進行。

connect作的事情僅僅是:

選擇器工廠配置對象傳給connectAdvanced進行進一步處理。

(彷佛「篩選和包裝」的功能是經過選擇器工廠和配置對象實現的,下面咱們來看看是否是如此。)

問題2

「篩選和包裝」的功能是如何實現的?


connectAdvanced

從上面的代碼能夠看出,connectAdvanced的做用是:

根據selectorFactory和配置對象,建立一個高階組件。

下面看看源碼:

function connectAdvanced(selectorFactory, { /*options配置對象*/ }) {
    // ... 根據options初始化一些內部變量
    
    //返回一個高階組件(輸入爲一個組件,輸出爲另外一個組件的函數)
    return function wrapWithConnect(WrappedComponent){
        // 高階組件返回的組件Connect
        class Connect extends Component {
            constructor(props, context) {
                super(props, context)

                this.state = {}
                // 從props或context讀取store
                //(由於WrappedComponent本身可能也是一個Provider)
                // 在本文的分析中咱們忽略Provider嵌套的這種狀況
                this.store = props[storeKey] || context[storeKey]
                // ...
            	this.initSelector() // 初始化一個selector(後面會講)
            }
            
            // 初始化selector的函數
            initSelector() {/*...*/}
            
            // ... 其餘一些生命週期函數和工具函數
        }
        Connect.contextTypes = contextTypes // 獲取外層的context
        Connect.propTypes = contextTypes
                            
        // 返回Connect組件的時候多了一步處理
        // 這個函數的做用是將WrappedComponent上的靜態方法拷貝到Connect上,並返回Connect
        // 這樣就能夠徹底把Connect看成一個「WrappedComponent」使用
        return hoistNonReactStatics(Connect, WrappedComponent);
    }
}
複製代碼

結構依舊很簡單,就是返回一個高階組件。所謂高階組件,實際上是一個函數。

高階組件接收被包裹的組件,返回一個Connect組件,下面咱們的重點就放在這個Connect組件是如何建立的。

constructor看起來很普通,只不過從context中獲取了store,而後將store存下來。還調用了一個initSelector()函數,初始化選擇器?選擇器是什麼東西???別急咱們一步一步來看。

看看其源碼:

initSelector() {
    // 傳入dispatch方法和配置對象,獲得一個原始選擇器
    const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)
    // 對這個原始選擇器進行進一步處理,獲得一個最終的"stateful"的選擇器selector
    this.selector = makeSelectorStateful(sourceSelector, this.store)
    // 調用selector的一個方法
    this.selector.run(this.props)
}
複製代碼

看完難免又有疑惑了:

  • selectorFactory到底作了什麼?
  • makeSelectorStateful作了什麼?添加的run方法是作什麼的?

一個一個來看。


1. selectorFactory----------------------------------
// ...

function impureFinalPropsSelectorFactory({ /*配置對象*/ }){
    // ...
}

function pureFinalPropsSelectorFactory({ /*配置對象*/ }){
    // ...
}

export default function finalPropsSelectorFactory(dispatch, { /*配置對象*/ }) {
  // 從配置對象拿到initMapStateToProps,initMapDispatchToProps,initMergeProps
  // 這些方法是對connect函數參數(mapStateToProps,mapDispatchToProps,mergeProps)的包裝
  // 因此調用後返回的是加強後,能夠直接使用的同名函數
  const mapStateToProps = initMapStateToProps(dispatch, options)
  const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
  const mergeProps = initMergeProps(dispatch, options)

  // 根據options的pure字段肯定使用哪一種工廠函數
  const selectorFactory = options.pure
    ? pureFinalPropsSelectorFactory
    : impureFinalPropsSelectorFactory

  // 使用選擇器工廠,返回一個選擇器
  return selectorFactory(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    dispatch,
    options //areStatesEqual, areOwnPropsEqual, areStatePropsEqual
  )
}
複製代碼

咱們來看看兩種工廠函數:

// pure爲false時的選擇器工廠
// 功能及其簡單,每次調用都返回一個新組裝的props
function impureFinalPropsSelectorFactory( mapStateToProps, mapDispatchToProps, mergeProps, dispatch ) {
  return function impureFinalPropsSelector(state, ownProps) {
    return mergeProps(
      mapStateToProps(state, ownProps),
      mapDispatchToProps(dispatch, ownProps),
      ownProps
    )
  }
}

// pure爲true時的選擇器工廠
function pureFinalPropsSelectorFactory({ mapStateToProps, mapDispatchToProps, mergeProps, dispatch, { areStatesEqual, areOwnPropsEqual, areStatePropsEqual } }){
    let hasRunAtLeastOnce = false // 是不是第一次使用選擇器
    let state // 這個state並非組件的狀態,而是redux的store
    let ownProps // 存儲上一次傳入的ownProps
    let stateProps // 經過mapStateToProps篩選出的要放進props的數據
    let dispatchProps // 經過mapDispatchToProps篩選出的要放進props的數據
    let mergedProps // 合併後的props,最終選擇器返回的就是這個參數。
    
    // 第一次調用選擇器的函數
    function handleFirstCall(firstState, firstOwnProps){
        state = firstState
        ownProps = firstOwnProps
        stateProps = mapStateToProps(state, ownProps)
        dispatchProps = mapDispatchToProps(dispatch, ownProps)
        mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
        hasRunAtLeastOnce = true
        return mergedProps
    }
    
    // 處理不一樣狀況時返回什麼props
    // 這三個函數也沒什麼稀奇的騷操做,就不展開了
    // 僅僅是根據須要調用mapStateToProps或mapDispatchToProps獲得stateProps和dispatchProps
    // 而後調用mergeProps獲得mergedProps
    function handleNewPropsAndNewState(){}
    function handleNewProps(){}
    function handleNewState(){}
    
    function handleSubsequentCalls(nextState, nextOwnProps) {
        // areOwnPropsEqual,areStatesEqual用於比較新舊state,props是否相同
        // 這是從配置對象中拿到的方法,實現方式就只是一個淺比較
        const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
        const stateChanged = !areStatesEqual(nextState, state)
        state = nextState
        ownProps = nextOwnProps

        if (propsChanged && stateChanged) return handleNewPropsAndNewState()
        if (propsChanged) return handleNewProps()
        if (stateChanged) return handleNewState()
        return mergedProps
    }
    
    // 這裏是返回的選擇器
    return function pureFinalPropsSelector(nextState, nextOwnProps) {
        return hasRunAtLeastOnce // 判斷是不是第一次使用選擇器
        	? handleSubsequentCalls(nextState, nextOwnProps)
        	: handleFirstCall(nextState, nextOwnProps) 
    }
}
複製代碼

能夠看出選擇器工廠返回了一個函數pureFinalPropsSelector,這就一個選擇器。

能夠看出,選擇器的功能,就是接收nextStatenextOwnProps,返回一個通過「篩選和包裝」的props。返回的props能夠直接傳給被包裹的組件。

回答問題2(「篩選和包裝」的功能是如何實現的?):

使用選擇器工廠,根據咱們傳進去的配置項(通過處理的mapXXXToProps,dispatch,淺比較方法),生成一個具備「篩選和包裝」功能的選擇器

connectAvanced中使用的selectorFactory已經弄明白了,下面看看另外一個makeSelectorStateful函數。

2. makeSelectorStateful---------------------------------
function makeSelectorStateful(sourceSelector, store) {
  // 建立了一個selector對象,這個對象有一個run方法
  const selector = {
    // run方法接收原始props(外部傳給被包裹組件的props),
    // 而且調用了一次原始選擇器,獲得調用後的props,
    // 將新props和內部緩存的舊props比較,
    // 根據結果,設置selector的shouldComponentUpdate屬性。
    run: function runComponentSelector(props) {
      try {
        const nextProps = sourceSelector(store.getState(), props)
        if (nextProps !== selector.props || selector.error) {
          selector.shouldComponentUpdate = true
          selector.props = nextProps
          selector.error = null
        }
      } catch (error) {
        selector.shouldComponentUpdate = true
        selector.error = error
      }
    }
  }

  return selector
}
複製代碼

如今再回到connectAdvanced的源碼:

function connectAdvanced(selectorFactory, { /*options配置對象*/ }) {
    //返回一個高階組件(輸入爲一個組件,輸出爲另外一個組件的函數)
    return function wrapWithConnect(WrappedComponent){
        // 高階組件返回的組件Connect
        class Connect extends Component {
            constructor(props, context) {
                super(props, context)

                this.state = {}
                // 從props或context讀取store
                this.store = props[storeKey] || context[storeKey]
            	this.initSelector() // 初始化一個選擇器
            }
            
            initSelector() {/*...*/}
            
            // 咱們如今要重點看這裏!!!!!!!!
            // ... 其餘一些生命週期函數和工具函數
        }
        // ...
        return hoistNonReactStatics(Connect, WrappedComponent);
    }
}
複製代碼

咱們已經知道選擇器的功能是:獲取「篩選和包裝」後的props,如今咱們看看Connect組件是如何使用選擇器的。將關注點放在Connect這個組件的生命週期函數是如何使用的:

class Connect extends Component {
    constructor(props, context) {
        // ...
        this.initSelector()
    }
            
    
    componentDidMount() {
        // 向store中添加監聽器,監聽器函數在下面
        // 就不詳細看這個函數的實現了,就是簡單的調用store的subscribe方法
        this.subscription.trySubscribe() 
        
        // 運行選擇器,根據選擇器運行後的結果判斷是否須要更新
        this.selector.run(this.props)
        if (this.selector.shouldComponentUpdate) this.forceUpdate()
    }
    // 當接收新的props時,運行選擇器
    componentWillReceiveProps(nextProps) {
        this.selector.run(nextProps)
    }
    // 根據選擇器運行後的結果判斷是否須要更新
    shouldComponentUpdate() {
        return this.selector.shouldComponentUpdate
    }
    // 卸載組件時清理內存
    componentWillUnmount() {
        // 卸載監聽器
        if (this.subscription) this.subscription.tryUnsubscribe()
        this.store = null
        this.selector.run = noop // 空函數function noop() {}
        this.selector.shouldComponentUpdate = false
    }
    
    // 監聽器,將會用subscribe方法添加到store上,每當store被dispatch會被調用
    onStateChange() {
        this.selector.run(this.props)
    }
    
    render() {
        const selector = this.selector
        selector.shouldComponentUpdate = false

        if (selector.error) {
          throw selector.error
        } else {
          // addExtraProps方法的做用時將selector篩選後的props,添加到本來的props上。
          return createElement(WrappedComponent, this.addExtraProps(selector.props))
        }
   }
}
複製代碼

原來這麼簡單啊,就只是:

  • 在須要的時候:

    • Connect組件第一次裝載組件時
    • Connect組件接收props時
    • 監聽store的監聽器被觸發時

    運行選擇器,獲得須要添加的額外的props

  • 根據運行的結果肯定是否更新Connect組件

  • 渲染時向被包裹組件添加額外的props。

總結

若是你看到了最後,你會發現,react-redux的實現方式和咱們文章開頭的解決方案一毛同樣:

  • store存在父組件的context上
  • 給子組件添加額外的props,以實現和store的交互

實現的亮點在於,react-redux用了高階組件這種優雅的方式,將這種需求進行了封裝。

若是有疑問,或者想要交流的地方,歡迎在評論區討論。

相關文章
相關標籤/搜索