React教程之Redux状态管理

Redux

虽然是react的一个状态管理库,可以单独使用,接下来我们详细讲解下底层代码。

废话不多讲,先说下基本的知识点。

Actions

actions 是通过 store.dispatch(action)改变 state的唯一方法,不能直接通过 this.state = {} 来直接改变 state

生成 actions 有两种方式:

1、单纯的 object

const action = {
  type: 'ADD_TODO',
  text: 'Build my first Redux app'
}
dispatch(action)

2、action creator function

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}
dispatch(addTodo())

Reducers

Actions 只能描述发生了什么,并不能描述状态发生了什么变化,Reducers 指定 state tree 将要发生什么。

const items = (state = [], action) => {
  switch (action.type) {
    case "ADD_ITEM":
      return [...state, { text: action.text }]
    default:
      return state
  }
}

这就是单个 reducer 的格式,固定两个参数 stateaction;

多个reducers 是需要使用 combineReducers,

import {combineReducers} from 'redux';

const items = (state = [], action) => {
  switch (action.type) {
    case "ADD_ITEM":
      return [...state, { text: action.text }]
    default:
      return state
  }
}

export default combineReducers({
  items
});

接下来,我们看看 combineReducers 里面实现了什么?

combineReducers


function combineReducers(reducers) {
  // 多个reducer
  var reducerKeys = Object.keys(reducers);
  // 最终的 reducers
  var finalReducers = {};

  for (var i = 0; i < reducerKeys.length; i++) {
    var key = reducerKeys[i];

    if (process.env.NODE_ENV !== 'production') {
      if (typeof reducers[key] === 'undefined') {
        warning("No reducer provided for key \"" + key + "\"");
      }
    }

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key];
    }
  }

  var finalReducerKeys = Object.keys(finalReducers);
  var unexpectedKeyCache;

  if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {};
  }

  var shapeAssertionError;

  try {
    //只是验证一下语法是否有错误
    assertReducerShape(finalReducers);
  } catch (e) {
    shapeAssertionError = e;
  }
  
  
  //返回一个 function(state, action){}
  // dispatch 会调用这里的 function
  return function combination(state, action) {
    if (state === void 0) {
      state = {};
    }

    if (shapeAssertionError) {
      throw shapeAssertionError;
    }

    if (process.env.NODE_ENV !== 'production') {
      var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache);

      if (warningMessage) {
        warning(warningMessage);
      }
    }

    var hasChanged = false;
    var nextState = {};

    for (var _i = 0; _i < finalReducerKeys.length; _i++) {
      var _key = finalReducerKeys[_i];
      //当前 key 的 reducer
      var reducer = finalReducers[_key];
      // 当前 key 的 state
      var previousStateForKey = state[_key];
      // 即将改变后的当前key 的 state
      var nextStateForKey = reducer(previousStateForKey, action);

      if (typeof nextStateForKey === 'undefined') {
        var errorMessage = getUndefinedStateErrorMessage(_key, action);
        throw new Error(errorMessage);
      }
      // 当前 reducer 的名字作为 key 保存在 state
      // 所以 当前例子 items 会变成 state.items = []
      nextState[_key] = nextStateForKey;
      // 判断状态是否发生改变,当下一个状态不等于上一个状态,标识状态已改变
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
    }
    
    // 返回 state
    return hasChanged ? nextState : state;
  };
}

Store

store是什么? store 是把 ActionsReducers 通过createStore结合起来的一个Object

打印一下:

Store: {
  getState,
  dispatch,
  subscribe
  replaceReducer,
}

createStore(reducer, preloadState, enhancer)


/**
 * Creates a Redux store that holds the state tree.
 * The only way to change the data in the store is to call `dispatch()` on it.
 *
 * There should only be a single store in your app. To specify how different
 * parts of the state tree respond to actions, you may combine several reducers
 * into a single reducer function by using `combineReducers`.
 *
 * @param {Function} reducer A function that returns the next state tree, given
 * the current state tree and the action to handle.
 *
 * @param {any} [preloadedState] The initial state. You may optionally specify it
 * to hydrate the state from the server in universal apps, or to restore a
 * previously serialized user session.
 * If you use `combineReducers` to produce the root reducer function, this must be
 * an object with the same shape as `combineReducers` keys.
 *
 * @param {Function} [enhancer] The store enhancer. You may optionally specify it
 * to enhance the store with third-party capabilities such as middleware,
 * time travel, persistence, etc. The only store enhancer that ships with Redux
 * is `applyMiddleware()`.
 *
 * @returns {Store} A Redux store that lets you read the state, dispatch actions
 * and subscribe to changes.
 */
export default function createStore(reducer, preloadedState, enhancer) {
  if (
    (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
    (typeof enhancer === 'function' && typeof arguments[3] === 'function')
  ) {
    throw new Error(
      'It looks like you are passing several store enhancers to ' +
        'createStore(). This is not supported. Instead, compose them ' +
        'together to a single function'
    )
  }

  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    return enhancer(createStore)(reducer, preloadedState)
  }

  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }

  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  /**
   * Reads the state tree managed by the store.
   *
   * @returns {any} The current state tree of your application.
   */
  function getState() {
    if (isDispatching) {
      throw new Error(
        'You may not call store.getState() while the reducer is executing. ' +
          'The reducer has already received the state as an argument. ' +
          'Pass it down from the top reducer instead of reading it from the store.'
      )
    }

    return currentState
  }

  /**
   * Adds a change listener. It will be called any time an action is dispatched,
   * and some part of the state tree may potentially have changed. You may then
   * call `getState()` to read the current state tree inside the callback.
   *
   * You may call `dispatch()` from a change listener, with the following
   * caveats:
   *
   * 1. The subscriptions are snapshotted just before every `dispatch()` call.
   * If you subscribe or unsubscribe while the listeners are being invoked, this
   * will not have any effect on the `dispatch()` that is currently in progress.
   * However, the next `dispatch()` call, whether nested or not, will use a more
   * recent snapshot of the subscription list.
   *
   * 2. The listener should not expect to see all state changes, as the state
   * might have been updated multiple times during a nested `dispatch()` before
   * the listener is called. It is, however, guaranteed that all subscribers
   * registered before the `dispatch()` started will be called with the latest
   * state by the time it exits.
   *
   * @param {Function} listener A callback to be invoked on every dispatch.
   * @returns {Function} A function to remove this change listener.
   */
  function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }

    if (isDispatching) {
      throw new Error(
        'You may not call store.subscribe() while the reducer is executing. ' +
          'If you would like to be notified after the store has been updated, subscribe from a ' +
          'component and invoke store.getState() in the callback to access the latest state. ' +
          'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
      )
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    
    // 保存监听 回调函数 等 dispatch 时候 统一执行
    nextListeners.push(listener)

    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      if (isDispatching) {
        throw new Error(
          'You may not unsubscribe from a store listener while the reducer is executing. ' +
            'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
        )
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

  /**
   * Dispatches an action. It is the only way to trigger a state change.
   *
   * The `reducer` function, used to create the store, will be called with the
   * current state tree and the given `action`. Its return value will
   * be considered the **next** state of the tree, and the change listeners
   * will be notified.
   *
   * The base implementation only supports plain object actions. If you want to
   * dispatch a Promise, an Observable, a thunk, or something else, you need to
   * wrap your store creating function into the corresponding middleware. For
   * example, see the documentation for the `redux-thunk` package. Even the
   * middleware will eventually dispatch plain object actions using this method.
   *
   * @param {Object} action A plain object representing “what changed”. It is
   * a good idea to keep actions serializable so you can record and replay user
   * sessions, or use the time travelling `redux-devtools`. An action must have
   * a `type` property which may not be `undefined`. It is a good idea to use
   * string constants for action types.
   *
   * @returns {Object} For convenience, the same action object you dispatched.
   *
   * Note that, if you use a custom middleware, it may wrap `dispatch()` to
   * return something else (for example, a Promise you can await).
   */
  function dispatch(action) {
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }

     // 这里说明 actions 必须包含 type 否则报错
    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }

     // 一次只能分派一个action
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      // dispatch分配时候,设置标识
      isDispatching = true
      // 使用 combination(state, action) 获取当前 state
      currentState = currentReducer(currentState, action)
    } finally {
      // 执行完毕 设置 false
      isDispatching = false
    }

     /**
     * 这里的 listeners 是 subcribe 订阅的 callback
     * 所以每次 dispatch , 都会触发 订阅的 callback
     */ 
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

  /**
   * Replaces the reducer currently used by the store to calculate the state.
   *
   * You might need this if your app implements code splitting and you want to
   * load some of the reducers dynamically. You might also need this if you
   * implement a hot reloading mechanism for Redux.
   *
   * @param {Function} nextReducer The reducer for the store to use instead.
   * @returns {void}
   */
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }

    currentReducer = nextReducer
    dispatch({ type: ActionTypes.REPLACE })
  }

  /**
   * Interoperability point for observable/reactive libraries.
   * @returns {observable} A minimal observable of state changes.
   * For more information, see the observable proposal:
   * https://github.com/tc39/proposal-observable
   */
  function observable() {
    const outerSubscribe = subscribe
    return {
      /**
       * The minimal observable subscription method.
       * @param {Object} observer Any object that can be used as an observer.
       * The observer object should have a `next` method.
       * @returns {subscription} An object with an `unsubscribe` method that can
       * be used to unsubscribe the observable from the store, and prevent further
       * emission of values from the observable.
       */
      subscribe(observer) {
        if (typeof observer !== 'object' || observer === null) {
          throw new TypeError('Expected the observer to be an object.')
        }

        function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }

        observeState()
        const unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },

      [$$observable]() {
        return this
      }
    }
  }

  // When a store is created, an "INIT" action is dispatched so that every
  // reducer returns their initial state. This effectively populates
  // the initial state tree.
  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}

这里的createStroe,在新建store的时候,会默认执行

dispatch({ type: ActionTypes.INIT })

然后执行,

currentState = currentReducer(currentState, action)

这里的 currentReducer 就是 createStrore(reducer) 里面的参数 reducer, 也就是combineReducers 函数的返回函数 combination(state, action), 让我们再回顾一下 combination 里面说的什么;

combination(state, action)


function combination(state, action) {
    // 设置 state = {}
    if (state === void 0) {
      state = {};
    }
    
    if (shapeAssertionError) {
      throw shapeAssertionError;
    }
    
    // 错误提示
    if (process.env.NODE_ENV !== 'production') {
      var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache);
    
      if (warningMessage) {
        warning(warningMessage);
      }
    }
    
    var hasChanged = false;
    var nextState = {};// 设置下一状态
       
    /**
    * 例子中有两个 reducer (items, location)
    * const items = (state = [], action) => {
          switch (action.type) {
            case "ADD_ITEM":
              return [...state, { text: action.text }]
            default:
              return state
          }
      }
    * const location = (state = window.location, action) => state;
    * 
    * combineReducers({items, location})
    * 此时的 finalReducerKeys = [items, location]
    * finalReducers={
         items: function items(){},
         location: function location()
      }
      
    * 实现过程:
    * 
    * loop 1:
    *   _key = items;
    *   reducer = function items(){}
    *  previousStateForKey = undefined
    *  nextStateForKey = items(undefined, {type: '@@redux/INIT.....'}) = []
    *  nextState = {items: []}
    *  
    * loop 2:
    *   _key = location;
    *  reducer = function location(){}
    *  previousStateForKey = undefined
    *  nextStateForKey = location(undefined, {type: '@@redux/INIT.....'}) = window.location
    *  nextState = {items: [], location: window.location}
    *  
    */
    for (var _i = 0; _i < finalReducerKeys.length; _i++) {
      var _key = finalReducerKeys[_i];
      var reducer = finalReducers[_key];
      // 读取 state 里面的上一个状态
      var previousStateForKey = state[_key];
      // 获取 下一个 state 状态 并且 合并数据
      var nextStateForKey = reducer(previousStateForKey, action);
    
      if (typeof nextStateForKey === 'undefined') {
        var errorMessage = getUndefinedStateErrorMessage(_key, action);
        throw new Error(errorMessage);
      }
    
      // 赋值返回 nextState
      nextState[_key] = nextStateForKey;
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
    }
    
    return hasChanged ? nextState : state;
};

getState()

getState 这个方法就很简单了,只是返回 currentState

subscribe(listener)

订阅这块,就是保持每一个listener callbacklisteners 数组里,等到 执行 dispatch(action), 再一个个循环执行。最有意思的就是返回值是一个 unsubscribe function, 顾名思义就是解除订阅,用法稍后再说。

replaceReducer(nextReducer)

其实就替换reducer, 一般热加载的时候会用到。

Usage with React

我们大致已经了解了 store 了, 但是如何结合 react 使用呢?

这里需要了解一下 react-redux 提供的 <Provide /> 组件,这是一个 container component, 用例:

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'

const store = createStore(todoApp)

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

Provider


import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { ReactReduxContext } from './Context'

class Provider extends Component {
  constructor(props) {
    super(props)
    
    // 获取 stroe
    const { store } = props
    this.state = {
      // 当前 state
      storeState: store.getState(),
      store
    }
  }

  componentDidMount() {
    this._isMounted = true
    this.subscribe()
  }

  componentWillUnmount() {
    if (this.unsubscribe) this.unsubscribe()

    this._isMounted = false
  }

  componentDidUpdate(prevProps) {
    if (this.props.store !== prevProps.store) {
      if (this.unsubscribe) this.unsubscribe()

      this.subscribe()
    }
  }

  subscribe() {
    const { store } = this.props

    this.unsubscribe = store.subscribe(() => {
      const newStoreState = store.getState()

      if (!this._isMounted) {
        return
      }

      this.setState(providerState => {
        // If the value is the same, skip the unnecessary state update.
        if (providerState.storeState === newStoreState) {
          return null
        }

        return { storeState: newStoreState }
      })
    })

    // Actions might have been dispatched between render and mount - handle those
    const postMountStoreState = store.getState()
    if (postMountStoreState !== this.state.storeState) {
      this.setState({ storeState: postMountStoreState })
    }
  }

  render() {
    const Context = this.props.context || ReactReduxContext

    return (
      <Context.Provider value={this.state}>
        {this.props.children}
      </Context.Provider>
    )
  }
}

Provider.propTypes = {
  store: PropTypes.shape({
    subscribe: PropTypes.func.isRequired,
    dispatch: PropTypes.func.isRequired,
    getState: PropTypes.func.isRequired
  }),
  context: PropTypes.object,
  children: PropTypes.any
}

export default Provider

如图:

[图片上传失败...(image-6cc47-1558534168137)]

Provide 可以传递 Store 给子组件, 但是此时 <App />

<App />
  props: {}
  state: {}

如果获取state<App />呢? 此时我们需要了解一下 connect

connect

function connect(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  {
    pure = true,
    areStatesEqual = strictEqual,
    areOwnPropsEqual = shallowEqual,
    areStatePropsEqual = shallowEqual,
    areMergedPropsEqual = shallowEqual,
    ...extraOptions
  } = {}
) {
  const initMapStateToProps = match(
    mapStateToProps,
    mapStateToPropsFactories,
    'mapStateToProps'
  )
  const initMapDispatchToProps = match(
    mapDispatchToProps,
    mapDispatchToPropsFactories,
    'mapDispatchToProps'
  )
  const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')
  
  /**
  * 默认:
  * connectHOC = connectAdvanced
    mapStateToPropsFactories = defaultMapStateToPropsFactories
    mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories
    mergePropsFactories = defaultMergePropsFactories
    selectorFactory = defaultSelectorFactory
  */
  return connectHOC(selectorFactory, {
    // used in error messages
    methodName: 'connect',

    // used to compute Connect's displayName from the wrapped component's displayName.
    getDisplayName: name => `Connect(${name})`,

    // if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes
    shouldHandleStateChanges: Boolean(mapStateToProps),

    // passed through to selectorFactory
    initMapStateToProps,
    initMapDispatchToProps,
    initMergeProps,
    pure,
    areStatesEqual,
    areOwnPropsEqual,
    areStatePropsEqual,
    areMergedPropsEqual,

    // any extra options args can override defaults of connect or connectAdvanced
    ...extraOptions
  })
}

// 在子组件
export default connect(
  ((state, ownProps) => {
    return {
      data: state.items
    }
  }),
  (dispatch, ownProps) => {
    return {
      addItem: () => {
        dispatch(addItem(ownProps.name))
      }
    }
  }
)(App);

connet(mapStateToProps, mapDispatchToProps, mapProps)(App) => connectHoc(App, {...opts}) => connectAdvanced(App)

关于 initMapStateToPropsinitMapDispatchToPropswrapMapToPropsFunc 初始化的方法稍微讲解一下


function match(arg, factories, name) {
  for (let i = factories.length - 1; i >= 0; i--) {
    const result = factories[i](arg)
    if (result) return result
  }

  return (dispatch, options) => {
    throw new Error(
      `Invalid value of type ${typeof arg} for ${name} argument when connecting component ${
        options.wrappedComponentName
      }.`
    )
  }
}

initMapStateToProps


 mapStateToProps: (state, ownProp)=> {return {state}}
 mapStateToPropsFactories: [whenMapStateToPropsIsFunction, whenMapStateToPropsIsMissing]
 
 //此时mapStateToProps 是个 function, 所以执行 whenMapStateToPropsIsFunction
  

whenMapStateToPropsIsFunction | whenMapStateToPropsIsMissing

export function whenMapStateToPropsIsFunction(mapStateToProps) {
  return typeof mapStateToProps === 'function'
    ? wrapMapToPropsFunc(mapStateToProps, 'mapStateToProps')
    : undefined
}

export function whenMapStateToPropsIsMissing(mapStateToProps) {
  return !mapStateToProps ? wrapMapToPropsConstant(() => ({})) : undefined
}

// Used by whenMapStateToPropsIsFunction and whenMapDispatchToPropsIsFunction,
// this function wraps mapToProps in a proxy function which does several things:
//
//  * Detects whether the mapToProps function being called depends on props, which
//    is used by selectorFactory to decide if it should reinvoke on props changes.
//
//  * On first call, handles mapToProps if returns another function, and treats that
//    new function as the true mapToProps for subsequent calls.
//
//  * On first call, verifies the first result is a plain object, in order to warn
//    the developer that their mapToProps function is not returning a valid result.
//
export function wrapMapToPropsFunc(mapToProps, methodName) {
  return function initProxySelector(dispatch, { displayName }) {
    const proxy = function mapToPropsProxy(stateOrDispatch, ownProps) {
      return proxy.dependsOnOwnProps
        ? proxy.mapToProps(stateOrDispatch, ownProps)
        : proxy.mapToProps(stateOrDispatch)
    }

    // allow detectFactoryAndVerify to get ownProps
    proxy.dependsOnOwnProps = true

    proxy.mapToProps = function detectFactoryAndVerify(
      stateOrDispatch,
      ownProps
    ) {
      proxy.mapToProps = mapToProps
      //判断是否 带有 props 作为参数
      proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps)
      let props = proxy(stateOrDispatch, ownProps)

      if (typeof props === 'function') {
        proxy.mapToProps = props
        proxy.dependsOnOwnProps = getDependsOnOwnProps(props)
        props = proxy(stateOrDispatch, ownProps)
      }

      if (process.env.NODE_ENV !== 'production')
        verifyPlainObject(props, displayName, methodName)

      return props
    }

    return proxy
  }
}

wrapMapToPropsFunc 返回一个 initProxySelector(dispatch, {displayName}) => proxy(stateOrDispatch, ownProps)

其余两个不再赘述,有空再讲解。

connectAdvanced


function connectAdvanced(
 
  selectorFactory,
  // 默认参数
  {
    // the func used to compute this HOC's displayName from the wrapped component's displayName.
    // probably overridden by wrapper functions such as connect()
    getDisplayName = name => `ConnectAdvanced(${name})`,

    // shown in error messages
    // probably overridden by wrapper functions such as connect()
    methodName = 'connectAdvanced',

    // REMOVED: if defined, the name of the property passed to the wrapped element indicating the number of
    // calls to render. useful for watching in react devtools for unnecessary re-renders.
    renderCountProp = undefined,

    // determines whether this HOC subscribes to store changes
    shouldHandleStateChanges = true,

    // REMOVED: the key of props/context to get the store
    storeKey = 'store',

    // REMOVED: expose the wrapped component via refs
    withRef = false,

    // use React's forwardRef to expose a ref of the wrapped component
    forwardRef = false,

    // the context consumer to use
    context = ReactReduxContext,

    // additional options are passed through to the selectorFactory
    ...connectOptions
  } = {}
) {

  /**
  * connectHOC 之前代码 一一对应
  * {
      // used in error messages
      methodName: 'connect',

      // used to compute Connect's displayName from the wrapped component's displayName.
      // connect(App)
      getDisplayName: name => `Connect(${name})`,

      // if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes
      shouldHandleStateChanges: Boolean(mapStateToProps),

      // passed through to selectorFactory
      initMapStateToProps,
      initMapDispatchToProps,
      initMergeProps,
      pure,
      areStatesEqual,
      areOwnPropsEqual,
      areStatePropsEqual,
      areMergedPropsEqual,

      // any extra options args can override defaults of connect or connectAdvanced
      ...extraOptions
    }
  */
  
  //React.createContext(null)
  const Context = context

  return function wrapWithConnect(WrappedComponent) {
    ....
    
    // 结果是 App
    const wrappedComponentName =
      WrappedComponent.displayName || WrappedComponent.name || 'Component'

    // Connect(App)
    const displayName = getDisplayName(wrappedComponentName)
    
    
    /**
    * 合并 options:
    * 
    * WrappedComponent: ƒ App(props)
      areMergedPropsEqual: ƒ shallowEqual(objA, objB)
      areOwnPropsEqual: ƒ shallowEqual(objA, objB)
      areStatePropsEqual: ƒ shallowEqual(objA, objB)
      areStatesEqual: ƒ strictEqual(a, b)
      displayName: "Connect(App)"
      getDisplayName: ƒ getDisplayName(name)
      initMapDispatchToProps: ƒ initProxySelector(dispatch, _ref)
      initMapStateToProps: ƒ initProxySelector(dispatch, _ref)
      initMergeProps: ƒ ()
      methodName: "connect"
      pure: true
      renderCountProp: undefined
      shouldHandleStateChanges: true
      storeKey: "store"
      wrappedComponentName: "App"  
    */
    const selectorFactoryOptions = {
      ...connectOptions,
      getDisplayName,
      methodName,
      renderCountProp,
      shouldHandleStateChanges,
      storeKey,
      displayName,
      wrappedComponentName,
      WrappedComponent
    }
    
    // true
    const { pure } = connectOptions

    let OuterBaseComponent = Component

    if (pure) {
      OuterBaseComponent = PureComponent
    }

    // 合并 props
    function makeDerivedPropsSelector() {
      let lastProps
      let lastState
      let lastDerivedProps
      let lastStore
      let lastSelectorFactoryOptions
      let sourceSelector

      return function selectDerivedProps(
        state,
        props,
        store,
        selectorFactoryOptions
      ) {
        if (pure && lastProps === props && lastState === state) {
          return lastDerivedProps
        }

        if (
          store !== lastStore ||
          lastSelectorFactoryOptions !== selectorFactoryOptions
        ) {
          lastStore = store
          lastSelectorFactoryOptions = selectorFactoryOptions
          sourceSelector = selectorFactory(
            store.dispatch,
            selectorFactoryOptions
          )
        }

        lastProps = props
        lastState = state

        const nextProps = sourceSelector(state, props)

        lastDerivedProps = nextProps
        return lastDerivedProps
      }
    }


    // 合并 props 到 组件 App 上
    function makeChildElementSelector() {
      let lastChildProps, lastForwardRef, lastChildElement, lastComponent

      return function selectChildElement(
        WrappedComponent,
        childProps,
        forwardRef
      ) {
        if (
          childProps !== lastChildProps ||
          forwardRef !== lastForwardRef ||
          lastComponent !== WrappedComponent
        ) {
          lastChildProps = childProps
          lastForwardRef = forwardRef
          lastComponent = WrappedComponent
          lastChildElement = (
            <WrappedComponent {...childProps} ref={forwardRef} />
          )
        }

        return lastChildElement
      }
    }

    class Connect extends OuterBaseComponent {
      constructor(props) {
        super(props)
        invariant(
          forwardRef ? !props.wrapperProps[storeKey] : !props[storeKey],
          'Passing redux store in props has been removed and does not do anything. ' +
            customStoreWarningMessage
        )
        this.selectDerivedProps = makeDerivedPropsSelector()
        this.selectChildElement = makeChildElementSelector()
        this.indirectRenderWrappedComponent = this.indirectRenderWrappedComponent.bind(
          this
        )
      }

      indirectRenderWrappedComponent(value) {
        // calling renderWrappedComponent on prototype from indirectRenderWrappedComponent bound to `this`
        return this.renderWrappedComponent(value)
      }
      
      // 此时value = {storeState, store} 
      renderWrappedComponent(value) {
        invariant(
          value,
          `Could not find "store" in the context of ` +
            `"${displayName}". Either wrap the root component in a <Provider>, ` +
            `or pass a custom React context provider to <Provider> and the corresponding ` +
            `React context consumer to ${displayName} in connect options.`
        )
        const { storeState, store } = value

        let wrapperProps = this.props
        let forwardedRef

        if (forwardRef) {
          wrapperProps = this.props.wrapperProps
          forwardedRef = this.props.forwardedRef
        }
        
        // 派生混合 props = {dataa: {}, items = {}}
        let derivedProps = this.selectDerivedProps(
          storeState,
          wrapperProps,
          store,
          selectorFactoryOptions
        )
        
        // 植入到 childElement
        return this.selectChildElement(
          WrappedComponent,
          derivedProps,
          forwardedRef
        )
      }

      render() {
        // 这里 ContextToUse == Context
        const ContextToUse =
          this.props.context &&
          this.props.context.Consumer &&
          isContextConsumer(<this.props.context.Consumer />)
            ? this.props.context
            : Context

        return (
          <ContextToUse.Consumer>
            {this.indirectRenderWrappedComponent}
          </ContextToUse.Consumer>
        )
      }
    }

    Connect.WrappedComponent = WrappedComponent
    Connect.displayName = displayName

    if (forwardRef) {
      const forwarded = React.forwardRef(function forwardConnectRef(
        props,
        ref
      ) {
        return <Connect wrapperProps={props} forwardedRef={ref} />
      })

      forwarded.displayName = displayName
      forwarded.WrappedComponent = WrappedComponent
      return hoistStatics(forwarded, WrappedComponent)
    }

    return hoistStatics(Connect, WrappedComponent)
  }
}


wrapWithConnect 这里最终生成的也是一个connect组件

[图片上传失败...(image-f8bbb9-1558534168138)]

现在我们从头梳理一下整个过程

首先通过 <Provider></Provider>包裹 <App />,

<Provider store={store}>
  <App />
</Provider>

Provider 中首先通过Props 获取到 store,

const { store } = props
this.state = {
  storeState: store.getState(),
  store
}

componentDidMount 之后发布了一个订阅去同步更新 state.storeState,

subscribe() {
  const { store } = this.props

  this.unsubscribe = store.subscribe(() => {
    const newStoreState = store.getState()

    if (!this._isMounted) {
      return
    }
    //同步更新state 然后同步 context value 更新后代组件
    this.setState(providerState => {
      // If the value is the same, skip the unnecessary state update.
      if (providerState.storeState === newStoreState) {
        return null
      }

      return { storeState: newStoreState }
    })
  })

  // Actions might have been dispatched between render and mount - handle those
  const postMountStoreState = store.getState()
  if (postMountStoreState !== this.state.storeState) {
    this.setState({ storeState: postMountStoreState })
  }
}

最后重点部分,state 是如何下传给后代组件<App />,

import { ReactReduxContext } from './Context'
render() {
  // this.props.context 显然为空,所以 Context = React.createContext(null)
  const Context = this.props.context || ReactReduxContext

  return (
    <Context.Provider value={this.state}>
       // 后代组件可以通过 value 去获取 state
      {this.props.children}
    </Context.Provider>
  )
}

此时的React Dom 结构是:

/**
* Provider:
*   props: {
*     store: {getState, dispatch, replaceReducer, subscribe},
*     chilren: ...
*   }
*   state: {
*     store: {getState, dispatch, replaceReducer, subscribe},
*     storeState: {item, location}
*   }
*   
*/
<Provider>
  /**
  * Context.Provider: 
  *    props:{
  *      children,
  *      value: {store, storeState}
  *    }
  */
  <Context.Provider>
    <App />
  </Context.Provider>
</Provider>

Provider组件的作用到此结束,接下就要说明 connect 是如何把 state 变成后代组件的 props

首先,需要调用 connect 方法把当前组件<App /> 进行包装处理:

export default connect(
  ((state, ownProps) => {
    return {
      data: state.items
    }
  }),
  (dispatch, ownProps) => {
    return {
      addItem: () => {
        dispatch(addItem(ownProps.name))
      }
    }
  }
)(App);

connet(mapStateToProps, mapDispatchToProps)(App), 最终经过一系列中间过程最终执行的其实就是 wrapWithConnect,这时候

import { ReactReduxContext } from './Context'
context = ReactReduxContext

注意:

此处的contextProvider 里面引用的 ReactReduxContext 是同一个,所以该组件将可以和 Provider 关联起来,可以获取最近的 <Context.Provider> 里面的 value = {store, storeState} 值。

wrapWithConnect

所以在 react-redux/src/components/connectAdvanced.js 中 wrapWithConnect 里面有这样的一个 render:


class Connect extends OuterBaseComponent {
  constructor(props) {
    super(props)
    ...
    this.selectDerivedProps = makeDerivedPropsSelector()
    this.selectChildElement = makeChildElementSelector()
    this.indirectRenderWrappedComponent = this.indirectRenderWrappedComponent.bind(
      this
    )
  }

  indirectRenderWrappedComponent(value) {
    // calling renderWrappedComponent on prototype from indirectRenderWrappedComponent bound to `this`
    return this.renderWrappedComponent(value)
  }

  renderWrappedComponent(value) {
    ....
    const { storeState, store } = value

    let wrapperProps = this.props
    let forwardedRef

    if (forwardRef) {
      wrapperProps = this.props.wrapperProps
      forwardedRef = this.props.forwardedRef
    }

    let derivedProps = this.selectDerivedProps(
      storeState,
      wrapperProps,
      store,
      selectorFactoryOptions
    )

    //生成新的组件
    return this.selectChildElement(
      WrappedComponent,
      derivedProps,
      forwardedRef
    )
  }

  render() {
    // 这里明显是 Context 还是刚才的那个 ReactReduxContext
    const ContextToUse =
      this.props.context &&
        this.props.context.Consumer &&
        isContextConsumer(<this.props.context.Consumer />)
        ? this.props.context
        : Context

    return (
      //Context.Consumer 可以绑定 value 到 component function 里面,也就是this.indirectRenderWrappedComponent(value)
      <ContextToUse.Consumer>
        {this.indirectRenderWrappedComponent}
      </ContextToUse.Consumer>
    )
  }
}

简单看下 this.indirectRenderWrappedComponent :

//生成新的组件
return this.selectChildElement(
  WrappedComponent,// <App />
  derivedProps,// {data, items}
  forwardedRef
)
function selectChildElement(
  WrappedComponent,
  childProps,
  forwardRef
) {
  if (
    childProps !== lastChildProps ||
    forwardRef !== lastForwardRef ||
    lastComponent !== WrappedComponent
  ) {
    lastChildProps = childProps
    lastForwardRef = forwardRef
    lastComponent = WrappedComponent
    lastChildElement = (
      <WrappedComponent {...childProps} ref={forwardRef} />
    )
  }

  return lastChildElement
}

前面经过一系列 mapStateToProps 和 mapDispatchToProps 的处理,最终:

<WrappedComponent {...childProps} ref={forwardRef} />
也就是
<App {...childProps} ref={forwardRef} />

到现在为止,state 怎么变成 props 已经很明显了。

最终 React Dom 结构如下:


/**
* Provider:
*   props: {
*     store: {getState, dispatch, replaceReducer, subscribe},
*     chilren: ...
*   }
*   state: {
*     store: {getState, dispatch, replaceReducer, subscribe},
*     storeState: {item, location}
*   }
*   
*/
<Provider>
  /**
  * Context.Provider: 
  *    props:{
  *      children,
  *      value: {store, storeState}
  *    }
  */
  <Context.Provider>
    // props: {}
    <Connect>
      // props:{children:bound indirectRenderWrappedComponent()}
      <Context.Consumer>
        // props: {data, items}
        <App />
      </Context.Consumer>
    </Connect>
  </Context.Provider>
</Provider>

Redux暂时完结,以后再详细讲解其中的一些部分。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容