React-Redux分析

zhjj511 7年前發布 | 17K 次閱讀 Redux 移動開發

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

索引

      • 3.1.1 container/TodoList.js
      • 3.1.2 components/TodoList.js
      • 3.1.3 components/Todo.js
  • 4 容器組件與展示型組件
  • 6 connectAdvanced高階函數
      • 6.1.1 pureFinalPropsSelectorFactory
      • 6.1.2 handleFirstCall
      • 6.1.3 defaultMergeProps
      • 6.1.4 handleSubsequentCalls
      • 6.1.5 計算返回新props
      • 6.1.6 計算返回stateProps
  • 7 hoist-non-react-statics

前言

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

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()源碼分析

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

 

來自: React-Redux分析

 

 本文由用戶 zhjj511 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!