React-Redux源碼分析

Redux,做爲大型React應用狀態管理最經常使用的工具,其概念理論和實踐都是很值得咱們學習,分析而後在實踐中深刻了解的,對前端開發者能力成長頗有幫助。本篇計劃結合Redux容器組件和展現型組件的區別對比以及Redux與React應用最多見的鏈接庫,react-redux源碼分析,以期達到對Redux和React應用的更深層次理解。html

歡迎訪問個人我的博客前端

前言

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

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

Provider

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

// 內部組件獲取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。git

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

Example

container/TodoList.js

首先咱們建立一個列表容器組件,在組件內負責獲取todo列表,而後將todos傳遞給TodoList展現型組件,同時傳遞事件回調函數,展現型組件觸發諸如點擊等事件時,調用對應回調,這些回調函數內經過dispatch actions來更新redux store state,而最終將store和展現型組件鏈接起來使用的是react-redux的connect方法,該方法接收redux

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

源碼markdown

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-
相關文章
相關標籤/搜索