React-Redux原始碼分析

熊建剛發表於2017-09-27

Redux,作為大型React應用狀態管理最常用的工具,其概念理論和實踐都是很值得我們學習,分析然後在實踐中深入瞭解的,對前端開發者能力成長很有幫助。本篇計劃結合Redux容器元件和展示型元件的區別對比以及Redux與React應用最常見的連線庫,react-redux原始碼分析,以期達到對Redux和React應用的更深層次理解。

歡迎訪問我的個人部落格

前言

react-redux庫提供Provider元件通過context方式嚮應用注入store,然後可以使用connect高階方法,獲取並監聽store,然後根據store state和元件自身props計算得到新props,注入該元件,並且可以通過監聽store,比較計算出的新props判斷是否需要更新元件。

react與redux應用結構
react與redux應用結構

Provider

首先,react-redux庫提供Provider元件將store注入整個React應用的某個入口元件,通常是應用的頂層元件。Provider元件使用context向下傳遞store:

// 內部元件獲取redux store的鍵
const storeKey = 'store'
// 內部元件
const subscriptionKey = subKey || `${storeKey}Subscription`
class Provider extends Component {
  // 宣告context,注入store和可選的釋出訂閱物件
  getChildContext() {
    return { [storeKey]: this[storeKey], [subscriptionKey]: null }
  }

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

  render() {
    // 渲染輸出內容
    return Children.only(this.props.children)
  }
}複製程式碼

Example

import { Provider } from 'react-redux'
import { createStore } from 'redux'
import App from './components/App'
import reducers from './reducers'

// 建立store
const store = createStore(todoApp, reducers)

// 傳遞store作為props給Provider元件;
// Provider將使用context方式向下傳遞store
// App元件是我們的應用頂層元件
render(
  <Provider store={store}>
    <App/>
  </Provider>, document.getElementById('app-node')
)複製程式碼

connect方法

在前面我們使用Provider元件將redux store注入應用,接下來需要做的是連線元件和store。而且我們知道Redux不提供直接操作store state的方式,我們只能通過其getState訪問資料,或通過dispatch一個action來改變store state。

這也正是react-redux提供的connect高階方法所提供的能力。

Example

container/TodoList.js

首先我們建立一個列表容器元件,在元件內負責獲取todo列表,然後將todos傳遞給TodoList展示型元件,同時傳遞事件回撥函式,展示型元件觸發諸如點選等事件時,呼叫對應回撥,這些回撥函式內通過dispatch actions來更新redux store state,而最終將store和展示型元件連線起來使用的是react-redux的connect方法,該方法接收

import {connect} from 'react-redux'
import TodoList from 'components/TodoList.jsx'

class TodoListContainer extends React.Component {
  constructor(props) {
    super(props)
    this.state = {todos: null, filter: null}
  }
  handleUpdateClick (todo) {
    this.props.update(todo);  
  }
  componentDidMount() {
    const { todos, filter, actions } = this.props
    if (todos.length === 0) {
      this.props.fetchTodoList(filter);
    }
  render () {
    const { todos, filter } = this.props

    return (
      <TodoList 
        todos={todos}
        filter={filter}
        handleUpdateClick={this.handleUpdateClick}
        /* others */
      />
    )
  }
}

const mapStateToProps = state => {
  return {
    todos : state.todos,
    filter: state.filter
  }
}

const mapDispatchToProps = dispatch => {
  return {
    update : (todo) => dispatch({
      type : 'UPDATE_TODO',
      payload: todo
    }),
    fetchTodoList: (filters) => dispatch({
      type : 'FETCH_TODOS',
      payload: filters
    })
  }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoListContainer)複製程式碼

components/TodoList.js

import React from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'

const TodoList = ({ todos, handleUpdateClick }) => (
  <ul>
    {todos.map(todo => (
      <Todo key={todo.id} {...todo} handleUpdateClick={handleUpdateClick} />
    ))}
  </ul>
)

TodoList.propTypes = {
  todos: PropTypes.array.isRequired
  ).isRequired,
  handleUpdateClick: PropTypes.func.isRequired
}

export default TodoList複製程式碼

components/Todo.js

import React from 'react'
import PropTypes from 'prop-types'

class Todo extends React.Component { 
  constructor(...args) {
    super(..args);
    this.state = {
      editable: false,
      todo: this.props.todo
    }
  }
  handleClick (e) {
    this.setState({
      editable: !this.state.editable
    })
  }
  update () {
    this.props.handleUpdateClick({
      ...this.state.todo
      text: this.refs.content.innerText
    })
  }
  render () {
    return (
      <li
        onClick={this.handleClick}
        style={{
          contentEditable: editable ? 'true' : 'false'
        }}
      >
        <p ref="content">{text}</p>
        <button onClick={this.update}>Save</button>
      </li>
    )
  }

Todo.propTypes = {
  handleUpdateClick: PropTypes.func.isRequired,
  text: PropTypes.string.isRequired
}

export default Todo複製程式碼

容器元件與展示型元件

在使用Redux作為React應用的狀態管理容器時,通常貫徹將元件劃分為容器元件(Container Components)和展示型元件(Presentational Components)的做法,

Presentational Components Container Components
目標 UI展示 (HTML結構和樣式) 業務邏輯(獲取資料,更新狀態)
感知Redux
資料來源 props 訂閱Redux store
變更資料 呼叫props傳遞的回撥函式 Dispatch Redux actions
可重用 獨立性強 業務耦合度高

應用中大部分程式碼是在編寫展示型元件,然後使用一些容器元件將這些展示型元件和Redux store連線起來。

connect()原始碼分析

react-redux原始碼邏輯
react-redux原始碼邏輯

connectHOC = connectAdvanced;
mergePropsFactories = defaultMergePropsFactories;
selectorFactory = defaultSelectorFactory;
function connect (
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  {
  pure = true,
  areStatesEqual = strictEqual, // 嚴格比較是否相等
  areOwnPropsEqual = shallowEqual, // 淺比較
  areStatePropsEqual = shallowEqual,
  areMergedPropsEqual = shallowEqual,
  renderCountProp, // 傳遞給內部元件的props鍵,表示render方法呼叫次數
  // props/context 獲取store的鍵
  storeKey = 'store',
  ...extraOptions
  } = {}
) {
  const initMapStateToProps = match(mapStateToProps, mapStateToPropsFactories, 'mapStateToProps')
  const initMapDispatchToProps = match(mapDispatchToProps, mapDispatchToPropsFactories, 'mapDispatchToProps')
  const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')

  // 呼叫connectHOC方法
  connectHOC(selectorFactory, {
    // 如果mapStateToProps為false,則不監聽store state
    shouldHandleStateChanges: Boolean(mapStateToProps),
    // 傳遞給selectorFactory
    initMapStateToProps,
    initMapDispatchToProps,
    initMergeProps,
    pure,
    areStatesEqual,
    areOwnPropsEqual,
    areStatePropsEqual,
    areMergedPropsEqual,
    renderCountProp, // 傳遞給內部元件的props鍵,表示render方法呼叫次數
    // props/context 獲取store的鍵
    storeKey = 'store',
    ...extraOptions // 其他配置項
  });
}複製程式碼

strictEquall

function strictEqual(a, b) { return a === b }複製程式碼

shallowEquall

原始碼

const hasOwn = Object.prototype.hasOwnProperty

function is(x, y) {
  if (x === y) {
    return x !== 0 || y !== 0 || 1 / x === 1 / y
  } else {
    return x !== x && y !== y
  }
}

export default function shallowEqual(objA, objB) {
  if (is(objA, objB)) return true

  if (typeof objA !== 'object' || objA === null ||
      typeof objB !== 'object' || objB === null) {
    return false
  }

  const keysA = Object.keys(objA)
  const keysB = Object.keys(objB)

  if (keysA.length !== keysB.length) return false

  for (let i = 0; i < keysA.length; i++) {
    if (!hasOwn.call(objB, keysA[i]) ||
        !is(objA[keysA[i]], objB[keysA[i]])) {
      return false
    }
  }

  return true
}複製程式碼
shallowEqual({x:{}},{x:{}}) // false
shallowEqual({x:1},{x:1}) // true複製程式碼

connectAdvanced高階函式

原始碼

function connectAdvanced (
  selectorFactory,
  {
    renderCountProp = undefined, // 傳遞給內部元件的props鍵,表示render方法呼叫次數
    // props/context 獲取store的鍵
    storeKey = 'store',
    ...connectOptions
  } = {}
) {
  // 獲取釋出訂閱器的鍵
  const subscriptionKey = storeKey + 'Subscription';
  const contextTypes = {
    [storeKey]: storeShape,
    [subscriptionKey]: subscriptionShape,
  };
  const childContextTypes = {
    [subscriptionKey]: subscriptionShape,
  };

  return function wrapWithConnect (WrappedComponent) {
    const selectorFactoryOptions = {
      // 如果mapStateToProps為false,則不監聽store state
      shouldHandleStateChanges: Boolean(mapStateToProps),
      // 傳遞給selectorFactory
      initMapStateToProps,
      initMapDispatchToProps,
      initMergeProps,
      ...connectOptions,
      ...others
      renderCountProp, // render呼叫次數
      shouldHandleStateChanges, // 是否監聽store state變更
      storeKey,
      WrappedComponent
    }

    // 返回擴充過props屬性的Connect元件
    return hoistStatics(Connect, WrappedComponent)
  }
}複製程式碼

selectorFactory

selectorFactory函式返回一個selector函式,根據store state, 展示型元件props,和dispatch計算得到新props,最後注入容器元件,selectorFactory函式結構形如:

(dispatch, options) => (state, props) => ({
  thing: state.things[props.thingId],
  saveThing: fields => dispatch(actionCreators.saveThing(props.thingId, fields)),
})複製程式碼

注:redux中的state通常指redux store的state而不是元件的state,另此處的props為傳入元件wrapperComponent的props。

原始碼

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

  // pure為true表示selectorFactory返回的selector將快取結果;
  // 否則其總是返回一個新物件
  const selectorFactory = options.pure
    ? pureFinalPropsSelectorFactory
    : impureFinalPropsSelectorFactory

  // 最終執行selector工廠函式返回一個selector
  return selectorFactory(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    dispatch,
    options
  );
}複製程式碼

pureFinalPropsSelectorFactory

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

  // 返回合併後的props或state
  // handleSubsequentCalls變更後合併;handleFirstCall初次呼叫
  return function pureFinalPropsSelector(nextState, nextOwnProps) {
    return hasRunAtLeastOnce
      ? handleSubsequentCalls(nextState, nextOwnProps)
    : handleFirstCall(nextState, nextOwnProps)
  }  
}複製程式碼

handleFirstCall

function handleFirstCall(firstState, firstOwnProps) {
  state = firstState
  ownProps = firstOwnProps
  stateProps = mapStateToProps(state, ownProps) // store state對映到元件的props
  dispatchProps = mapDispatchToProps(dispatch, ownProps)
  mergedProps = mergeProps(stateProps, dispatchProps, ownProps) // 合併後的props
  hasRunAtLeastOnce = true
  return mergedProps
}複製程式碼

defaultMergeProps

export function defaultMergeProps(stateProps, dispatchProps, ownProps) {
  // 預設合併props函式
  return { ...ownProps, ...stateProps, ...dispatchProps }
}複製程式碼

handleSubsequentCalls

function handleSubsequentCalls(nextState, nextOwnProps) {
  // shallowEqual淺比較
  const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
  // 深比較
  const stateChanged = !areStatesEqual(nextState, state)
  state = nextState
  ownProps = nextOwnProps

  // 處理props或state變更後的合併
  // store state及元件props變更
  if (propsChanged && stateChanged) return handleNewPropsAndNewState()
  if (propsChanged) return handleNewProps()
  if (stateChanged) return handleNewState()

  return mergedProps
}複製程式碼

計算返回新props

只要展示型元件自身props發生變更,則需要重新返回新合併props,然後更新容器元件,無論store state是否變更:

// 只有展示型元件props變更
function handleNewProps() {
  // mapStateToProps計算是否依賴於展示型元件props
  if (mapStateToProps.dependsOnOwnProps)
    stateProps = mapStateToProps(state, ownProps)
  // mapDispatchToProps計算是否依賴於展示型元件props
  if (mapDispatchToProps.dependsOnOwnProps)
    dispatchProps = mapDispatchToProps(dispatch, ownProps)

  mergedProps = mergeProps(stateProps, dispatchProps, ownProps)

  return mergedProps
}
// 展示型元件props和store state均變更
function handleNewPropsAndNewState() {
  stateProps = mapStateToProps(state, ownProps)
  // mapDispatchToProps計算是否依賴於展示型元件props
  if (mapDispatchToProps.dependsOnOwnProps)
    dispatchProps = mapDispatchToProps(dispatch, ownProps)

  mergedProps = mergeProps(stateProps, dispatchProps, ownProps)

  return mergedProps
}複製程式碼

計算返回stateProps

通常容器元件props變更由store state變更推動,所以只有store state變更的情況較多,而且此處也正是使用Immutable時需要注意的地方:不要在mapStateToProps方法內使用toJS()方法。

mapStateToProps兩次返回的props物件未有變更時,不需要重新計算,直接返回之前合併得到的props物件即可,之後在selector追蹤物件中比較兩次selector函式返回值是否有變更時,將返回false,容器元件不會觸發變更。

因為對比多次mapStateToProps返回的結果時是使用淺比較,所以不推薦使用Immutable.toJS()方法,其每次均返回一個新物件,對比將返回false,而如果使用Immutable且其內容未變更,則會返回true,可以減少不必要的重新渲染。

// 只有store state變更
function handleNewState() {
  const nextStateProps = mapStateToProps(state, ownProps)
  // 淺比較
  const statePropsChanged = !areStatePropsEqual(nextStateProps, stateProps)
  stateProps = nextStateProps

  // 計算得到的新props變更了,才需要重新計算返回新的合併props
  if (statePropsChanged) {
    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
  }

  // 若新stateProps未發生變更,則直接返回上一次計算得出的合併props;
  // 之後selector追蹤物件比較兩次返回值是否有變更時將返回false;
  // 否則返回使用mergeProps()方法新合併得到的props物件,變更比較將返回true
  return mergedProps
}複製程式碼

hoist-non-react-statics

類似Object.assign,將子元件的非React的靜態屬性或方法複製到父元件,React相關屬性或方法不會被覆蓋而是合併。

hoistStatics(Connect, WrappedComponent)複製程式碼

Connect Component

真正的Connect高階元件,連線redux store state和傳入元件,即將store state對映到元件props,react-redux使用Provider元件通過context方式注入store,然後Connect元件通過context接收store,並新增對store的訂閱:

class Connect extends Component {
  constructor(props, context) {
    super(props, context)

    this.state = {}
    this.renderCount = 0 // render呼叫次數初始為0
    // 獲取store,props或context方式
    this.store = props[storeKey] || context[storeKey]
    // 是否使用props方式傳遞store
    this.propsMode = Boolean(props[storeKey])

    // 初始化selector
    this.initSelector()
    // 初始化store訂閱
    this.initSubscription()
  }

  componentDidMount() {
    // 不需要監聽state變更
    if (!shouldHandleStateChanges) return
    // 釋出訂閱器執行訂閱
    this.subscription.trySubscribe()
    // 執行selector
    this.selector.run(this.props)
    // 若還需要更新,則強制更新
    if (this.selector.shouldComponentUpdate) this.forceUpdate()
  }

  // 渲染元件元素
  render() {
    const selector = this.selector
    selector.shouldComponentUpdate = false; // 重置是否需要更新為預設的false

    // 將redux store state轉化對映得到的props合併入傳入的元件
    return createElement(WrappedComponent, this.addExtraProps(selector.props))
  }
}複製程式碼

addExtraProps()

給props新增額外的props屬性:

// 新增額外的props
addExtraProps(props) {
  const withExtras = { ...props }
  if (renderCountProp) withExtras[renderCountProp] = this.renderCount++;// render 呼叫次數
  if (this.propsMode && this.subscription) withExtras[subscriptionKey] = this.subscription

  return withExtras
}複製程式碼

初始化selector追蹤物件initSelector

Selector,選擇器,根據redux store state和元件的自身props,計算出將注入該元件的新props,並快取新props,之後再次執行選擇器時通過對比得出的props,決定是否需要更新元件,若props變更則更新元件,否則不更新。

使用initSelector方法初始化selector追蹤物件及相關狀態和資料:

// 初始化selector
initSelector() {
  // 使用selector工廠函式建立一個selector
  const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)
  // 連線元件的selector和redux store state
  this.selector = makeSelectorStateful(sourceSelector, this.store)
  // 執行元件的selector函式
  this.selector.run(this.props)
}複製程式碼

makeSelectorStateful()

建立selector追蹤物件以追蹤(tracking)selector函式返回結果:

function makeSelectorStateful(sourceSelector, store) {
  // 返回selector追蹤物件,追蹤傳入的selector(sourceSelector)返回的結果
  const selector = {
    // 執行元件的selector函式
    run: function runComponentSelector(props) {
      // 根據store state和元件props執行傳入的selector函式,計算得到nextProps
      const nextProps = sourceSelector(store.getState(), props)
      // 比較nextProps和快取的props;
      // false,則更新所快取的props並標記selector需要更新
      if (nextProps !== selector.props || selector.error) {
        selector.shouldComponentUpdate = true // 標記需要更新
        selector.props = nextProps // 快取props
        selector.error = null
      }  
    }
  }

  // 返回selector追蹤物件
  return selector
}複製程式碼

初始化訂閱initSubscription

初始化監聽/訂閱redux store state:

// 初始化訂閱
initSubscription() {
  if (!shouldHandleStateChanges) return; // 不需要監聽store state

  // 判斷訂閱內容傳遞方式:props或context,兩者不能混雜
  const parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey]
  // 訂閱物件例項化,並傳入事件回撥函式
  this.subscription = new Subscription(this.store, 
                                       parentSub,
                                       this.onStateChange.bind(this))
  // 快取訂閱器釋出方法執行的作用域
  this.notifyNestedSubs = this.subscription.notifyNestedSubs
    .bind(this.subscription)
}複製程式碼

訂閱類實現

元件訂閱store使用的訂閱釋出器實現:

export default class Subscription {
  constructor(store, parentSub, onStateChange) {
    // redux store
    this.store = store
    // 訂閱內容
    this.parentSub = parentSub
    // 訂閱內容變更後的回撥函式
    this.onStateChange = onStateChange
    this.unsubscribe = null
    // 訂閱記錄陣列
    this.listeners = nullListeners
  }

  // 訂閱
  trySubscribe() {
    if (!this.unsubscribe) {
      // 若傳遞了釋出訂閱器則使用該訂閱器訂閱方法進行訂閱
      // 否則使用store的訂閱方法
      this.unsubscribe = this.parentSub
        ? this.parentSub.addNestedSub(this.onStateChange)
        : this.store.subscribe(this.onStateChange)

      // 建立訂閱集合物件
      // { notify: function, subscribe: function }
      // 內部包裝了一個釋出訂閱器;
      // 分別對應釋出(執行所有回撥),訂閱(在訂閱集合中新增回撥)
      this.listeners = createListenerCollection()
    }
  }

  // 釋出
  notifyNestedSubs() {
    this.listeners.notify()
  }
}複製程式碼

訂閱回撥函式

訂閱後執行的回撥函式:

onStateChange() {
  // 選擇器執行
  this.selector.run(this.props)

  if (!this.selector.shouldComponentUpdate) {
    // 不需要更新則直接釋出
    this.notifyNestedSubs()
  } else {
    // 需要更新則設定元件componentDidUpdate生命週期方法
    this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate
    // 同時呼叫setState觸發元件更新
    this.setState(dummyState) // dummyState = {}
  }
}

// 在元件componentDidUpdate生命週期方法內釋出變更
notifyNestedSubsOnComponentDidUpdate() {
  // 清除元件componentDidUpdate生命週期方法
  this.componentDidUpdate = undefined
  // 釋出
  this.notifyNestedSubs()
}複製程式碼

其他生命週期方法

getChildContext () {
  // 若存在props傳遞了store,則需要對其他從context接收store並訂閱的後代元件隱藏其對於store的訂閱;
  // 否則將父級的訂閱器對映傳入,給予Connect元件控制釋出變化的順序流
  const subscription = this.propsMode ? null : this.subscription
  return { [subscriptionKey]: subscription || this.context[subscriptionKey] }
}
// 接收到新props
componentWillReceiveProps(nextProps) {
  this.selector.run(nextProps)
}

// 是否需要更新元件
shouldComponentUpdate() {
  return this.selector.shouldComponentUpdate
}

componentWillUnmount() {
  // 重置selector
}複製程式碼

參考閱讀

  1. React with redux
  2. Smart and Dumb Components
  3. React Redux Container Pattern-

相關文章