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

村上春樹發表於2018-09-26

第一篇連結: redux真的不復雜——原始碼解讀

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

瞭解redux原理更好,如果不瞭解,不妨先看看第一篇部落格。

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

所以redux具有很強的適應性,可以配合其他工具/框架一起使用。

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

為什麼需要redux

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

有,state。但是state有一些侷限性。

對於一個元件來說,(用setState觸發)state的變化會觸發這個元件以及它所有子元件的更新,所以為了優化考慮,我們往往將state放在更“區域性”的元件中,這樣state的變化只會引起最少的(有必要的)更新。

那麼如果我們的react應用有一些狀態在多個地方都可能用到,特別是一些全域性資料(比如當前使用者資訊,全域性的通知等等),對於這些資料我們有兩個選擇:

  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,我們來看看原始碼的結構:

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

還挺複雜,不過沒關係,看看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用了高階元件這種優雅的方式,將這種需求進行了封裝。

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

相關文章