react-redux原始碼分析及實現原型(上)

opt_bbt發表於2018-09-02

redux作為大型應用的狀態管理工具,如果想配合react使用,需要藉助react-redux。 redux主要完成兩件事情:

  • 負責應用的狀態管理,保證單向資料流
  • 當應用狀態發生變化,觸發監聽器。

那麼,如果想要將react和redux搭配使用,就需要react元件可以根據redux中所儲存的狀態(store)更新view。 並且可以改變store。其實react-redux主要就是完成了這兩件事情。 第一,通過將store傳入root元件的context,使子節點可以獲取到 state。 第二,通過store.subscribe 訂閱store的變化,更新元件。 另外還有對於效能的優化,減少不必要的渲染。

熟悉使用方法

首先我們熟悉一下react-redux的基本使用

import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { createStore } from 'redux';
import { Provider, connect } from 'react-redux';

const reducer = (state, action) => {
  if (action.type === 'add') {
    return {
      ...state,
      count: state.count + 1,
    }
  }
  return state;
};

const store = createStore(reducer, { count: 1 });

const mapStateToProps = (state) => {
  return ({
    count: state.count,
  });
};

const mapDispatchToProps = dispatch => ({
  add: () => dispatch({ type: 'add' }),
});

const mergeProps = (state, props) =>({
  countStr: `計數: ${state.count}`,
  ...props,
});

const options = {
  pure: true,
};

class App extends React.Component {
  render() {
    return (
      <div>
        <p>{this.props.countStr}</p>
        <button onClick={this.props.add}>點選+1</button>
      </div>
    )
  }
}

const AppContainer = connect(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  options,
)(App);

ReactDOM.render(
  <Provider store={store}>
    <AppContainer />
  </Provider>,
  document.getElementById('root'));
複製程式碼

從上面的例子中我們可以看出,react-redux使用非常簡單,僅僅使用了兩個API,Providerconnect

  • Provider: 接收從redux而來的store,以供子元件使用。
  • connect: 高階元件,當元件需要獲取或者想要改變store的時候使用。可以接受四個引數:
    • mapStateToProps:取store資料,傳遞給元件。
    • mapDispatchToProps:改變store資料。
    • mergeProps:可以在其中對 mapStateToProps, mapDispatchToProps的結果進一步處理
    • options:一些配置項,例如 pure.當設定為true時,會避免不必要的渲染

原始碼解讀

Provider

class Provider extends Component {
  getChildContext() {
    return { [storeKey]: this[storeKey], [subscriptionKey]: null }
  }

  constructor(props, context) {
    super(props, context)
    this[storeKey] = props.store;
  }

  render() {
    return Children.only(this.props.children)
  }
}
複製程式碼

夠簡單吧,僅僅是把store放在了context下。subscriptionKey 可以暫時不用管。

connect

首先我們先回憶一下connect使用方法:

connect(mapStateToProps,mapDispatchToProps,mergeProps,options)(App);
複製程式碼

connect原始碼:

export function createConnect({
  connectHOC = connectAdvanced,
  mapStateToPropsFactories = defaultMapStateToPropsFactories,
  mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
  mergePropsFactories = defaultMergePropsFactories,
  selectorFactory = defaultSelectorFactory
} = {}) {
  return function connect(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    {
      pure = true,
      // ...
    } = {}
  ) {
    // 封裝了傳入的mapStateToProps等函式,這裡可以先理解為 initMapStateToProps = () => mapStateToProps 這種形式
    const initMapStateToProps = match(mapStateToProps, mapStateToPropsFactories, 'mapStateToProps')
    const initMapDispatchToProps = match(mapDispatchToProps, mapDispatchToPropsFactories, 'mapDispatchToProps')
    const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')

    // 選擇器(selector)的作用就是計算mapStateToProps,mapDispatchToProps, ownProps(來自父元件的props)的結果,
    // 並將結果傳給子元件props。這就是為什麼你在mapStateToProps等三個函式中寫的結果,子元件可以通過this.props拿到。
    // 選擇器工廠函式(selectorFactory)作用就是建立selector
    return connectHOC(selectorFactory, {
      // 如果沒有傳,那麼將不會監聽state變化
      shouldHandleStateChanges: Boolean(mapStateToProps),

      // 傳給selectorFactory的引數
      initMapStateToProps,
      initMapDispatchToProps,
      initMergeProps,
      pure,
      // ...
    })
  }
}

export default createConnect()
複製程式碼

這段我們可以知道,connect是對connectHOC(connectAdvanced)的封裝,connectAdvanced使用了 defaultSelectorFactory,來建立selector。 react-redux 預設的 selectorFactory 中包含了很多效能優化的部分(我們一會兒會看到)。 其實react-redux 也提供了connectAdvanced API,為了便於大家理解我通過改變開頭的例子,瞭解一下selectorFactory 是如何工作的。

// const AppContainer = connect(
//   mapStateToProps,
//   mapDispatchToProps,
//   mergeProps,
//   options,
// )(App);

// 在之前我們使用了connect,現在我們使用 connectAdvanced 來實現一下。
// 主要是是實現 selectorFactory:
function selectorFactory(dispatch) {
  let result = {}
  const action = mapDispatchToProps(dispatch);

  return (nextState, nextOwnProps) => {
    const state = mapStateToProps(nextState);
    const nextResult = mergeProps(state, action, nextOwnProps);
    if (!shallowEqual(result, nextResult)) result = nextResult
    return result;
  }
}
const AppContainer = connectAdvanced(selectorFactory)(App);
複製程式碼

這是一個簡單的 selectorFactory,主要體現了其是如何工作的,讓大家有一個大概的瞭解。 下面來看一下react-redux是如何實現 selectorFactory 的

selectorFactory

在解讀程式碼之前,我想先說一下options.pure的作用。不知道大家還記得嗎,在開頭的例子有一個配置項pure。作用是減少運算優化效能。當設定為false時,react-redux將不會優化,store.subscirbe事件觸發,元件就會渲染,即使是沒有用到的state更新,也會這樣,舉個例子

// 大家都知道reducer的寫法
return {
  ...state,
  count: state.count + 1,
}

// 必須返回一個新物件,那麼如果我只是修改原來 state 比如
state.count += 1;
return state;

// 你會發現元件將不會渲染,其實store裡的值是發生變化的。
// 這時如果你非想這麼寫, 然後又想重新渲染怎麼辦?
// 就是將pure設定為false,這樣元件就會完全實時更新。
複製程式碼

舉了個不恰當的例子,大家千萬不要這麼幹。。。

原始碼:

export default function finalPropsSelectorFactory(dispatch, {
  initMapStateToProps,
  initMapDispatchToProps,
  initMergeProps,
  ...options
}) {
  const mapStateToProps = initMapStateToProps(dispatch, options)
  const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
  const mergeProps = initMergeProps(dispatch, options)

  if (process.env.NODE_ENV !== 'production') {
    verifySubselectors(mapStateToProps, mapDispatchToProps, mergeProps, options.displayName)
  }

  const selectorFactory = options.pure
    ? pureFinalPropsSelectorFactory
    : impureFinalPropsSelectorFactory

  return selectorFactory(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    dispatch,
    options
  )
}
複製程式碼

這裡很好理解,是對 selectorFactory 的封裝,根據 options.pure 的值,選取不同的 SelectorFactory;

  • options.pure 為 false 時,使用 impureFinalPropsSelectorFactory
  • options.pure 為 true 時,使用 pureFinalPropsSelectorFactory

先來看看簡單的 impureFinalPropsSelectorFactory,其實和之前實現的 selectorFactory,差不多,無腦計算返回新值。

export function impureFinalPropsSelectorFactory(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  dispatch
) {
  return function impureFinalPropsSelector(state, ownProps) {
    return mergeProps(
      mapStateToProps(state, ownProps),
      mapDispatchToProps(dispatch, ownProps),
      ownProps
    )
  }
}
複製程式碼

怎麼樣,和之前自己實現的差不多吧,自己實現的還有一個淺對比呢~ 笑哭

pureFinalPropsSelectorFactory

export function pureFinalPropsSelectorFactory(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  dispatch,
  { areStatesEqual, areOwnPropsEqual, areStatePropsEqual }
) {
  let hasRunAtLeastOnce = false
  let state
  let ownProps
  let stateProps
  let dispatchProps
  let mergedProps

  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
  }

  function handleNewPropsAndNewState() {
    stateProps = mapStateToProps(state, ownProps)

    if (mapDispatchToProps.dependsOnOwnProps)
      dispatchProps = mapDispatchToProps(dispatch, ownProps)

    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
    return mergedProps
  }

  function handleNewProps() {
    // ...
  }

  function handleNewState() {
    // ...
  }

  function handleSubsequentCalls(nextState, nextOwnProps) {
    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)
  }
}
複製程式碼

一句話: 在需要的時候才執行mapStateToProps,mapDispatchToProps,mergeProps 為了減少篇幅,挑部分講解。其實這篇已經很長了有沒有,不知道看到這裡的你,犯困了沒有? 還是已經睡著了? 上個圖醒醒腦

Anne Hathaway

言歸正傳,這裡也分兩種情況:

  • 當第一次執行時,執行handleFirstCall,將三個map函式執行一遍,並將結果快取下來
  • 之後執行 handleSubsequentCalls,在其中將新的值和快取的值做比較,如果變化,將重新求值並返回,如果沒變化,返回快取的舊值。

其中 mapFunction.dependsOnOwnProps 代表你傳入的mapStateToProps是否使用了ownProps。 如果沒有使用,那麼props的變化將不會影響結果,換句話說對應的mapFunction將不會執行。 判斷方法也很簡單,就是獲取function形參長度,如何獲得呢? mdn function.length

總結

這邊文章主要講了react-redux使用方法以及分析了原始碼中 Provider、connect、selectorFactory。中間也穿插了一些demo方便大家理解。 到目前為止大家應該已經熟悉了整個框架的工作流程。由於感覺篇幅過長,所以決定分為兩期來講解。下面一期中主要是剩下的 connectHOC(connectAdvanced),這個才是react-redux的核心。 希望看完的朋友沒有浪費你們時間,有所幫助,有什麼意見儘管提,就當你們自己是產品(?)好le.

我的github

寫完又讀了一遍,感覺篇幅其實不長,想想應該是自己寫的累了。。。

react-redux原始碼分析及實現原型(下)

相關文章