「源码解析」一文吃透react-redux源码(useMemo经典源码级案例)

前言

使用过redux的同窗都知道,redux做为react公共状态管理工具,配合react-redux能够很好的管理数据,派发更新,更新视图渲染的做用,那么对于 react-redux 是如何作到根据 state 的改变,而更新组件,促使视图渲染的呢,让咱们一块儿来探讨一下,react-redux 源码的奥妙所在。前端

在正式分析以前咱们不妨来想几个问题:
vue

1 为何要在 root 根组件上使用 react-reduxProvider 组件包裹?
2 react-redux 是怎么和 redux 契合,作到 state 改变动新视图的呢?
3 provide 用什么方式存放当前的 reduxstore, 又是怎么传递给每个须要管理state的组件的?
4 connect 是怎么样链接咱们的业务组件,而后传递咱们组件更新函数的呢?
5 connect 是怎么经过第一个参数,来订阅与之对应的 state 的呢?
6 connect 怎么样将 props,和 reduxstate 合并的?react

带着这些疑问咱们不妨先看一下 Provider 究竟作了什么?算法

一 Provider 建立Subscription,context保存上下文

/* provider 组件代码 */
function Provider({ store, context, children }) {
   /* 利用useMemo,跟据store变化建立出一个contextValue 包含一个根元素订阅器和当前store */ 
  const contextValue = useMemo(() => {
      /* 建立了一个根 Subscription 订阅器 */
    const subscription = new Subscription(store)
    /* subscription 的 notifyNestedSubs 方法 ,赋值给 onStateChange方法 */
    subscription.onStateChange = subscription.notifyNestedSubs  
    return {
      store,
      subscription
    } /* store 改变建立新的contextValue */
  }, [store])
  /* 获取更新以前的state值 ,函数组件里面的上下文要优先于组件更新渲染 */
  const previousState = useMemo(() => store.getState(), [store])

  useEffect(() => {
    const { subscription } = contextValue
    /* 触发trySubscribe方法执行,建立listens */
    subscription.trySubscribe() // 发起订阅
    if (previousState !== store.getState()) {
        /* 组件更新渲染以后,若是此时state发生改变,那么当即触发 subscription.notifyNestedSubs 方法 */
      subscription.notifyNestedSubs() 
    }
    /* */
    return () => {
      subscription.tryUnsubscribe()  // 卸载订阅
      subscription.onStateChange = null
    }
    /* contextValue state 改变出发新的 effect */
  }, [contextValue, previousState])

  const Context = context || ReactReduxContext
  /* context 存在用跟元素传进来的context ,若是不存在 createContext建立一个context ,这里的ReactReduxContext就是由createContext建立出的context */
  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}

复制代码

从源码中provider做用大体是这样的

1 首先建立一个 contextValue ,里面包含一个建立出来的父级 Subscription (咱们姑且先称之为根级订阅器)和redux提供的store
2 经过react上下文contextcontextValue 传递给子孙组件。redux

二 Subscription订阅消息,发布更新

在咱们分析了不是很长的 provider 源码以后,随之一个 Subscription 出现,那么这个 Subscription 由什么做用呢🤔🤔🤔,咱们先来看看在 Provder 里出现的Subscription 方法。小程序

notifyNestedSubs trySubscribe tryUnsubscribe设计模式

在整个 react-redux 执行过程当中 Subscription 做用很是重要,这里方便先透漏一下,他的做用是收集全部被 connect 包裹的组件的更新函数 onstatechange,而后造成一个 callback 链表,再由父级 Subscription 统一派发执行更新,咱们暂且不关心它是怎么运做的,接下来就是 Subscription 源码 ,咱们重点看一下如上出现的三个方法。前端工程化

/* 发布订阅者模式 */
export default class Subscription {
  constructor(store, parentSub) {
    this.store = store
    this.parentSub = parentSub
    this.unsubscribe = null
    this.listeners = nullListeners

    this.handleChangeWrapper = this.handleChangeWrapper.bind(this)
  }
  /* 负责检测是否该组件订阅,而后添加订阅者也就是listener */
  addNestedSub(listener) {
    this.trySubscribe()
    return this.listeners.subscribe(listener)
  }
  /* 向listeners发布通知 */
  notifyNestedSubs() {
    this.listeners.notify()
  }
  /* 对于 provide onStateChange 就是 notifyNestedSubs 方法,对于 connect 包裹接受更新的组件 ,onStateChange 就是 负责更新组件的函数 。 */
  handleChangeWrapper() {
    if (this.onStateChange) {
      this.onStateChange()
    }
  }
   /* 判断有没有开启订阅 */
  isSubscribed() {
    return Boolean(this.unsubscribe)
  }
  /* 开启订阅模式 首先判断当前订阅器有没有父级订阅器 , 若是有父级订阅器(就是父级Subscription),把本身的handleChangeWrapper放入到监听者链表中 */
  trySubscribe() {
    /* parentSub 便是provide value 里面的 Subscription 这里能够理解为 父级元素的 Subscription */
    if (!this.unsubscribe) {
      this.unsubscribe = this.parentSub
        ? this.parentSub.addNestedSub(this.handleChangeWrapper)
        /* provider的Subscription是不存在parentSub,因此此时trySubscribe 就会调用 store.subscribe */
        : this.store.subscribe(this.handleChangeWrapper)
      this.listeners = createListenerCollection()
    }
  }
  /* 取消订阅 */
  tryUnsubscribe() {
    if (this.unsubscribe) {
      this.unsubscribe()
      this.unsubscribe = null
      this.listeners.clear()

      this.listeners = nullListeners
    }
  }
}


复制代码

看完 ProviderSubscription源码,我来解释一下二者到底有什么关联,首先Provider建立 Subscription 时候没有第二个参数,就说明provider 中的Subscription 不存在 parentSub 。 那么再调用Provider组件中useEffect钩子中trySubscribe的时候,会触发this.store.subscribe , subscribe 就是 reduxsubscribe ,此时真正发起了订阅。数组

subscription.onStateChange = subscription.notifyNestedSubs 
复制代码

有此可知,最终state改变,触发的是notifyNestedSubs方法。咱们再一次看看这个notifyNestedSubs缓存

/* 向listeners发布通知 */
notifyNestedSubs() {
  this.listeners.notify()
}
复制代码

最终向当前Subscription 的订阅者们发布 notify更新。

Subscription总结 - 发布订阅模式的实现

综上所述咱们总结一下。Subscription 的做用,首先经过 trySubscribe 发起订阅模式,若是存在这父级订阅者,就把本身更新函数handleChangeWrapper,传递给父级订阅者,而后父级由 addNestedSub 方法将此时的回调函数(更新函数)添加到当前的 listeners 中 。若是没有父级元素(Provider的状况),则将此回调函数放在store.subscribe中,handleChangeWrapper 函数中onStateChange,就是 ProviderSubscriptionnotifyNestedSubs 方法,而 notifyNestedSubs 方法会通知listensnotify 方法来触发更新。这里透漏一下,子代Subscription会把更新自身handleChangeWrapper传递给parentSub,来统一通知connect组件更新。

这里咱们弄明白一个问题

react-redux 更新组件也是用了 store.subscribe 并且 store.subscribe 只用在了 ProviderSubscription中 (没有 parentsub )

大体模型就是

state更改 -> store.subscribe -> 触发 providerSubscriptionhandleChangeWrapper 也就是 notifyNestedSubs -> 通知 listeners.notify() -> 通知每一个被 connect 容器组件的更新 -> callback 执行 -> 触发子组件Subscription 的 handleChangeWrapper ->触发子 onstatechange(能够提早透漏一下,onstatechange保存了更新组件的函数)。

前边的内容提到了**createListenerCollection,listeners**,可是他具体有什么做用咱们接下来一块儿看一下。

function createListenerCollection() {
   /* batch 由getBatch获得的 unstable_batchedUpdates 方法 */
  const batch = getBatch()
  let first = null
  let last = null

  return {
    /* 清除当前listeners的全部listener */
    clear() {
      first = null
      last = null
    },
    /* 派发更新 */
    notify() {
      batch(() => {
        let listener = first
        while (listener) {
          listener.callback()
          listener = listener.next
        }
      })
    },
    /* 获取listeners的全部listener */
    get() {
      let listeners = []
      let listener = first
      while (listener) {
        listeners.push(listener)
        listener = listener.next
      }
      return listeners
    },
     /* 接收订阅,将当前的callback(handleChangeWrapper)存到当前的链表中 */
    subscribe(callback) {
      let isSubscribed = true

      let listener = (last = {
        callback,
        next: null,
        prev: last
      })

      if (listener.prev) {
        listener.prev.next = listener
      } else {
        first = listener
      }
      /* 取消当前 handleChangeWrapper 的订阅*/
      return function unsubscribe() {
        if (!isSubscribed || first === null) return
        isSubscribed = false

        if (listener.next) {
          listener.next.prev = listener.prev
        } else {
          last = listener.prev
        }
        if (listener.prev) {
          listener.prev.next = listener.next
        } else {
          first = listener.next
        }
      }
    }
  }
}
复制代码

batch

import { unstable_batchedUpdates as batch } from './utils/reactBatchedUpdates'
setBatch(batch)
复制代码

咱们能够得出结论 createListenerCollection 能够产生一个 listenerslisteners的做用。

1收集订阅: 以链表的形式收集对应的 listeners (每个Subscription) 的handleChangeWrapper函数。
2派发更新:, 经过 batch 方法( react-dom 中的 unstable_batchedUpdates ) 来进行批量更新。

舒适提示: React unstable_batchedUpdate() API 容许将一次事件循环中的全部 React 更新都一块儿批量处理到一个渲染过程当中。

总结

🤔到这里咱们明白了:

1 react-redux 中的 provider 做用 ,经过 reactcontext 传递 subscriptionredux 中的store ,而且创建了一个最顶部根 Subscription

2 Subscription 的做用:起到发布订阅做用,一方面订阅 connect 包裹组件的更新函数,一方面经过 store.subscribe 统一派发更新。

3 Subscription 若是存在这父级的状况,会把自身的更新函数,传递给父级 Subscription 来统一订阅。

三 connect 究竟作了什么?

1 回顾 connect 用法

工慾善其事,必先利其器 ,想要吃透源码以前,必须深度熟悉其用法。才能知其然知其因此然。咱们先来看看高阶组件connect用法。

function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?) 复制代码

mapStateToProps

const mapStateToProps = state => ({ todos: state.todos })
复制代码

做用很简单,组件依赖reduxstate,映射到业务组件的 props中,state改变触发,业务组件props改变,触发业务组件更新视图。当这个参数没有的时候,当前组件不会订阅 store 的改变。

mapDispatchToProps

const mapDispatchToProps = dispatch => {
  return {
    increment: () => dispatch({ type: 'INCREMENT' }),
    decrement: () => dispatch({ type: 'DECREMENT' }),
    reset: () => dispatch({ type: 'RESET' })
  }
}
复制代码

redux 中的dispatch 方法,映射到,业务组件的props中。

mergeProps

/* * stateProps , state 映射到 props 中的内容 * dispatchProps, dispatch 映射到 props 中的内容。 * ownProps 组件自己的 props */
(stateProps, dispatchProps, ownProps) => Object
复制代码

正常状况下,若是没有这个参数,会按照以下方式进行合并,返回的对象能够是,咱们自定义的合并规则。咱们还能够附加一些属性。

{ ...ownProps, ...stateProps, ...dispatchProps }
复制代码

options

{
  context?: Object,   // 自定义上下文
  pure?: boolean, // 默认为 true , 当为 true 的时候 ,除了 mapStateToProps 和 props ,其余输入或者state 改变,均不会更新组件。
  areStatesEqual?: Function, // 当pure true , 比较引进store 中state值 是否和以前相等。 (next: Object, prev: Object) => boolean
  areOwnPropsEqual?: Function, // 当pure true , 比较 props 值, 是否和以前相等。 (next: Object, prev: Object) => boolean
  areStatePropsEqual?: Function, // 当pure true , 比较 mapStateToProps 后的值 是否和以前相等。 (next: Object, prev: Object) => boolean
  areMergedPropsEqual?: Function, // 当 pure 为 true 时, 比较 通过 mergeProps 合并后的值 , 是否与以前等 (next: Object, prev: Object) => boolean
  forwardRef?: boolean, //当为true 时候,能够经过ref 获取被connect包裹的组件实例。
}
复制代码

options能够是如上属性,上面已经标注了每个属性的做用,这里就很少说了。

2 connect 初探

对于connect 组件 ,咱们先看源码一探究竟

/src/connect/connect.js

export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory } = {}) {
  return function connect( mapStateToProps, mapDispatchToProps, mergeProps, { pure = true, areStatesEqual = strictEqual, areOwnPropsEqual = shallowEqual, areStatePropsEqual = shallowEqual, areMergedPropsEqual = shallowEqual, ...extraOptions } = {} ) {
   
     /* 通过代理包装后的 mapStateToProps */
    const initMapStateToProps = match( mapStateToProps, mapStateToPropsFactories,'mapStateToProps' )
    /* 通过代理包装后的 mapDispatchToProps */
    const initMapDispatchToProps = match(  mapDispatchToProps, mapDispatchToPropsFactories,'mapDispatchToProps')
     /* 通过代理包装后的 mergeProps */
    const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')

    return connectHOC(selectorFactory, {
     
      methodName: 'connect',
      getDisplayName: name => `Connect(${name})`,
      shouldHandleStateChanges: Boolean(mapStateToProps),
      initMapStateToProps,
      initMapDispatchToProps,
      initMergeProps,
      pure,
      areStatesEqual,
      areOwnPropsEqual,
      areStatePropsEqual,
      areMergedPropsEqual,
      ...extraOptions
    })
  }
}

export default /*#__PURE__*/ createConnect()
复制代码

咱们先来分析一下整个函数作的事。

1 首先定一个 createConnect方法。 传入了几个默认参数,有两个参数很是重要,connectHOC 做为整个 connect 的高阶组件。selectorFactory 作为整合connect更新过程当中的造成新props的主要函数。默认的模式是pure模式。

2 而后执行createConnect方法,返回真正的connect函数自己。connect接收几个参数,而后和默认的函数进行整合,包装,代理,最后造成三个真正的初始化函数,这里的过程咱们就先不讲了。咱们接下来分别介绍这三个函数的用途。

initMapStateToProps ,用于造成真正的 MapStateToProps函数,将 store 中 state ,映射到 props

initMapDispatchToProps,用于造成真正的 MapDispatchToProps,将 dispatch 和 自定义的 dispatch 注入到props

initMergeProps,用于造成真正的 mergeProps函数,合并业务组件的 props , state 映射的 props , dispatch 映射的 props

这里有一个函数很是重要,这个函数就是mergeProps, 请你们记住这个函数,由于这个函数是判断整个connect是否更新组件关键所在。上边说过 connect基本用法的时候说过,当咱们不向connect传递第三个参数mergeProps 的时候,默认的defaultMergeProps以下

/src/connect/mergeProps.js

export function defaultMergeProps(stateProps, dispatchProps, ownProps) {
  return { ...ownProps, ...stateProps, ...dispatchProps }
}
复制代码

这个函数返回了一个新的对象,也就是新的props。并且将 业务组件 props , store 中的 state ,和 dispatch 结合到一块儿,造成一个新对象,做为新的 props 传递给了业务组件。

3 selectorFactory 造成新的props

前面说到selectorFactory 很重要,用于造成新的props,咱们记下来看selectorFactory 源码。

/src/connect/selectorFactory.js

export default function finalPropsSelectorFactory( dispatch, { initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options } ) {
  // mapStateToProps mapDispatchToProps mergeProps 为真正connect 通过一层代理的 proxy 函数
  const mapStateToProps = initMapStateToProps(dispatch, options)
  const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
  const mergeProps = initMergeProps(dispatch, options)

  const selectorFactory = options.pure ? pureFinalPropsSelectorFactory : impureFinalPropsSelectorFactory
   // 返回一个 函数用于生成新的 props 
  return selectorFactory(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    dispatch,
    options
  )
}
复制代码

finalPropsSelectorFactory 的代码很简单, 首先获得真正connect 通过一层代理函数 mapStateToProps ,mapDispatchToProps ,mergeProps。而后调用selectorFactory (在pure模式下,selectorFactory 就是 pureFinalPropsSelectorFactory ) 。

能够这里反复用了闭包,能够刚开始有点蒙,不过静下心来看发现其实不是很难。因为默认是pure,因此咱们接下来主要看 pureFinalPropsSelectorFactory 函数作了些什么。

/** pure组件处理 , 对比 props 是否发生变化 而后 合并props */
export function pureFinalPropsSelectorFactory( mapStateToProps, mapDispatchToProps, mergeProps, dispatch, { areStatesEqual, areOwnPropsEqual, areStatePropsEqual } //判断 state prop 是否相等 ) {
  let hasRunAtLeastOnce = false
  let state
  let ownProps
  let stateProps
  let dispatchProps
  let mergedProps
 
  /* 第一次 直接造成 ownProps stateProps dispatchProps 合并 造成新的 props */
  function handleFirstCall(firstState, firstOwnProps) {
    state = firstState
    ownProps = firstOwnProps
    stateProps = mapStateToProps(state, ownProps)
    dispatchProps = mapDispatchToProps(dispatch, ownProps)
    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
    hasRunAtLeastOnce = true
    return mergedProps
  }
  
  function handleNewPropsAndNewState() {
    // props 和 state 都改变 mergeProps 
  }

  function handleNewProps() {
    // props 改变 mergeProps
  }

  function handleNewState() {
     // state 改变 mergeProps
  }

  /* 不是第一次的状况 props 或者 store.state 发生改变的状况。 */
  function handleSubsequentCalls(nextState, nextOwnProps) {
      /* 判断两次 props 是否相等 */
    const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps) 
      /* 判断两次 store.state 是否相等 */
    const stateChanged = !areStatesEqual(nextState, state)
    state = nextState
    ownProps = nextOwnProps
    
    if (propsChanged && stateChanged) return handleNewPropsAndNewState()
    if (propsChanged) return handleNewProps()
    if (stateChanged) return handleNewState()
    return mergedProps
  }

  return function pureFinalPropsSelector(nextState, nextOwnProps) {
    return hasRunAtLeastOnce
      ? handleSubsequentCalls(nextState, nextOwnProps)
      : handleFirstCall(nextState, nextOwnProps)
  }
}
复制代码

这个函数处理逻辑很清晰。大体上作了这些事。经过闭包的形式返回一个函数pureFinalPropsSelectorpureFinalPropsSelector经过判断是不是第一次初始化组件。

若是是第一次,那么直接调用mergeProps合并ownProps,stateProps,dispatchProps 造成最终的props。 若是不是第一次,那么判断究竟是props仍是 store.state 发生改变,而后针对那里变化,从新生成对应的props,最终合并到真正的props

整个 selectorFactory 逻辑就是造成新的props传递给咱们的业务组件。

4 connectAdvanced 造成真正包裹业务组件的 Hoc

接下来咱们看一下 connect 返回的 connectAdvanced()到底作了什么,为了方便你们理解connect,咱们这里先看看 connect 用法。

正常模式下:

const mapStateToProp = (store) => ({ userInfo: store.root.userInfo })

function Index(){
    /* ..... */
    return <div> { /* .... */ } </div>
}
export default connect(mapStateToProp)(Index)
复制代码

装饰器模式下:

const mapStateToProp = (store) => ({ userInfo: store.root.userInfo })

@connect(mapStateToProp)
class Index extends React.Component{
    /* .... */
    render(){
        return <div> { /* .... */ } </div>
    }
}

复制代码

咱们上面讲到,connect执行 接受 mapStateToProp 等参数,最后返回 connectAdvanced() ,那么上述例子中connect执行第一步connect(mapStateToProp)===connectAdvanced(),也就是connectAdvanced()执行返回真正的hoc,用于包裹咱们的业务组件。

接下来咱们看 connectAdvanced 代码

/src/components/connectAdvanced.js

export default function connectAdvanced( selectorFactory, // 每次 props,state改变执行 ,用于生成新的 props。 { getDisplayName = name => `ConnectAdvanced(${name})`, //可能被包装函数(如connect())重写 methodName = 'connectAdvanced', //若是定义了,则传递给包装元素的属性的名称,指示要呈现的调用。用于监视react devtools中没必要要的从新渲染。 renderCountProp = undefined, shouldHandleStateChanges = true, //肯定此HOC是否订阅存储更改 storeKey = 'store', withRef = false, forwardRef = false, // 是否 用 forwarRef 模式 context = ReactReduxContext,// Provider 保存的上下文 ...connectOptions } = {} ) {
  /* ReactReduxContext 就是store存在的context */
  const Context = context
   /* WrappedComponent 为connect 包裹的组件自己 */   
  return  function wrapWithConnect(WrappedComponent){
      // WrappedComponent 被 connect 的业务组件自己
  }
}
复制代码

connectAdvanced接受配置参数 , 而后返回真正的 HOC wrapWithConnect

// 咱们能够讲下面的表达式分解
connect(mapStateToProp)(Index)

// 执行 connect
connect(mapStateToProp) 
//返回 
connectAdvanced()
//返回HOC
wrapWithConnect

复制代码

接下来咱们分析一下wrapWithConnect到底作了些什么?

5 wrapWithConnect 高阶组件

接下来咱们来一块儿研究一下 wrapWithConnect,咱们重点看一下 wrapWithConnect做为高阶组件,会返回一个组件,这个组件会对原有的业务组件,进行一系列加强等工做。

function wrapWithConnect(WrappedComponent) {
    const wrappedComponentName =
      WrappedComponent.displayName || WrappedComponent.name || 'Component'
  
    const displayName = getDisplayName(wrappedComponentName)
  
    const selectorFactoryOptions = {
      ...connectOptions,
      getDisplayName,
      methodName,
      renderCountProp,
      shouldHandleStateChanges,
      storeKey,
      displayName,
      wrappedComponentName,
      WrappedComponent
    }
    const { pure } = connectOptions
    function createChildSelector(store) {
      // 合并函数 mergeprops 获得最新的props
      return selectorFactory(store.dispatch, selectorFactoryOptions)
    }
    //判断是不是pure纯组件模式 若是是 将用 useMemo 提高性能
    const usePureOnlyMemo = pure ? useMemo : callback => callback()
    // 负责更新的容器子组件
    function ConnectFunction (props){
        // props 为 业务组件 真正的 props 
    }
    const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction
  
    Connect.WrappedComponent = WrappedComponent
    Connect.displayName = displayName
    /* forwardRef */
    if (forwardRef) {
      const forwarded = React.forwardRef(function forwardConnectRef( props, ref ) {
        return <Connect {...props} reactReduxForwardedRef={ref} />
      })
  
      forwarded.displayName = displayName
      forwarded.WrappedComponent = WrappedComponent
      return hoistStatics(forwarded, WrappedComponent)
    }
  
    return hoistStatics(Connect, WrappedComponent)
  }
}
复制代码

wrapWithConnect 的作的事大体分为一下几点:

第一步

1 声明负责更新的 ConnectFunction 无状态组件。和负责合并 propscreateChildSelector方法

第二步

2 判断是不是 pure 纯组件模式,若是是用react.memo包裹,这样作的好处是,会向 pureComponent 同样对 props 进行浅比较。

第三步

3 若是 connectforwardRef配置项,用React.forwardRef处理,这样作好处以下。

正常状况下由于咱们的WrappedComponentconnect 包装,因此不能经过ref访问到业务组件WrappedComponent的实例。

子组件

const mapStateToProp = (store) => ({ userInfo: store.root.userInfo })

class Child extends React.Component{
    render(){
        /* ... */
    }
}
export default connect(mapStateToProp)(Child)
复制代码

父组件

class Father extends React.Compoent{
    child = null 
    render(){
        return <Child ref={(cur)=> this.child = cur } { /* 获取到的不是`Child`自己 */ } />
    }
}
复制代码

咱们没法经过 ref 访问到 Child 组件。

因此咱们能够经过 optionsforwardRef 属性设置为 true,这样就能够根本解决问题。

connect(mapStateToProp,mapDispatchToProps,mergeProps,{ forwardRef:true  })(Child)
复制代码

第四步

hoistStatics(Connect, WrappedComponent)
复制代码

最后作的事情就是经过hoistStatics库 把子组件WrappedComponent的静态方法/属性,继承到父组件Connect上。由于在 高阶组件 包装 业务组件的过程当中,若是不对静态属性或是方法加以额外处理,是不会被包装后的组件访问到的,因此须要相似hoistStatics这样的库,来作处理。

接下来说的就是整个 connect的核心了。咱们来看一下负责更新的容器ConnectFunction 到底作了些什么?

6 ConnectFunction 控制更新

ConnectFunction 的代码很复杂,须要咱们一步步去吃透,一步步去消化。

function ConnectFunction(props) {
      /* TODO: 第一步 把 context ForwardedRef props 取出来 */
      const [
        reactReduxForwardedRef,
        wrapperProps // props 传递的props
      ] = useMemo(() => {
       
        const { reactReduxForwardedRef, ...wrapperProps } = props
        return [reactReduxForwardedRef, wrapperProps]
      }, [props])
   
  
      // 获取 context内容 里面含有 redux 中store 和 subscription
      const contextValue = useContext(Context)

      //TODO: 判断 store 是否来此 props didStoreComeFromProps ,正常状况下 ,prop 中是不存在 store 因此 didStoreComeFromProps = false
      const didStoreComeFromProps =
        Boolean(props.store) &&
        Boolean(props.store.getState) &&
        Boolean(props.store.dispatch)
      const didStoreComeFromContext =
        Boolean(contextValue) && Boolean(contextValue.store)
  
      // 获取 redux 中 store
      const store = didStoreComeFromProps ? props.store : contextValue.store
       // 返回merge函数 用于生成真正传给子组件 props
      const childPropsSelector = useMemo(() => {
        return createChildSelector(store)
      }, [store])


      // TODO: 第二步 subscription 监听者实例 
      const [subscription, notifyNestedSubs] = useMemo(() => {
          // 若是没有订阅更新,那么直接返回。
        if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY
  
        const subscription = new Subscription(
          store,
          didStoreComeFromProps ? null : contextValue.subscription // 和 上级 `subscription` 创建起关系。 this.parentSub = contextValue.subscription
        )
        // notifyNestedSubs 触发 noticy 全部子代 listener 监听者 -> 触发batch方法,触发 batchupdate方法 ,批量更新
        const notifyNestedSubs = subscription.notifyNestedSubs.bind(
          subscription
        )
  
        return [subscription, notifyNestedSubs]
      }, [store, didStoreComeFromProps, contextValue])

      /* 建立出一个新的contextValue ,把父级的 subscription 换成本身的 subscription */
      const overriddenContextValue = useMemo(() => {   
        if (didStoreComeFromProps) { 
          return contextValue
        }
        return {
          ...contextValue,
          subscription
        }
      }, [didStoreComeFromProps, contextValue, subscription])
      const [
        [previousStateUpdateResult],
        forceComponentUpdateDispatch  /* */
      ] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)
  

      // TODO: 第三步 
      const lastChildProps = useRef() //保存上一次 合并过的 props信息(通过 ownprops ,stateProps , dispatchProps 合并过的 )
      const lastWrapperProps = useRef(wrapperProps) // 保存本次上下文执行 业务组件的 props 
      const childPropsFromStoreUpdate = useRef()
      const renderIsScheduled = useRef(false) // 当前组件是否处于渲染阶段
      // actualChildProps 为当前真正处理事后,通过合并的 props
      const actualChildProps = usePureOnlyMemo(() => {
          // 调用 mergeProps 进行合并,返回合并后的最新 porps
        return childPropsSelector(store.getState(), wrapperProps)
      }, [store, previousStateUpdateResult, wrapperProps])

     /* 负责更新缓存变量,方便下一次更新的时候比较 */
      useEffect(()=>{
        captureWrapperProps(...[
            lastWrapperProps,
            lastChildProps,
            renderIsScheduled,
            wrapperProps,
            actualChildProps,
            childPropsFromStoreUpdate,
            notifyNestedSubs
         ])
      })
     
      useEffect(()=>{
          subscribeUpdates(...[
          shouldHandleStateChanges,
          store,
          subscription,
          childPropsSelector,
          lastWrapperProps,
          lastChildProps,
          renderIsScheduled,
          childPropsFromStoreUpdate,
          notifyNestedSubs,
          forceComponentUpdateDispatch
         ])
      },[store, subscription, childPropsSelector])



      // TODO: 第四步:reactReduxForwardedRef 是处理父级元素是否含有 forwardRef 的状况 这里能够忽略。
      const renderedWrappedComponent = useMemo(
        () => (
          <WrappedComponent {...actualChildProps} ref={reactReduxForwardedRef} />
        ),
        [reactReduxForwardedRef, WrappedComponent, actualChildProps]
      )
      const renderedChild = useMemo(() => {
        //shouldHandleStateChanges 来源 connect是否有第一个参数
        if (shouldHandleStateChanges) {
          return (
            // ContextToUse 传递 context 
            <ContextToUse.Provider value={overriddenContextValue}> {renderedWrappedComponent} </ContextToUse.Provider>
          )
        }
  
        return renderedWrappedComponent
      }, [ContextToUse, renderedWrappedComponent, overriddenContextValue])
  
      return renderedChild
    }
复制代码

为了方便你们更直观的理解,我这里保留了影响流程的核心代码,我会一步步分析 整个核心部分。想要弄明白这里,须要对 react-hooksprovider 有一些了解。

第一步

经过 props 分离出 reactReduxForwardedRef , wrapperPropsreactReduxForwardedRef 是当开启 ForwardedRef 模式下,父级传过来的 React.forwaedRef

而后判断经过常量didStoreComeFromProps储存当前,redux.store 是否来自 props, 正常状况下,咱们的 store 都来自 provider ,不会来自props,因此咱们能够把didStoreComeFromProps = true 。接下来咱们获取到 store,经过 store 来判断是否更新真正的合并props函数childPropsSelector

第二步 建立 子代 subscription, 层层传递新的 context(很重要)

这一步很是重要,判断经过shouldHandleStateChanges判断此 HOC 是否订阅存储更改,若是已经订阅了更新(此时connect 具备第一个参数),那么建立一个 subscription ,而且和上一层providersubscription创建起关联。this.parentSub = contextValue.subscription。而后分离出 subscriptionnotifyNestedSubs(notifyNestedSubs的做用是通知当前subscriptionlisteners 进行更新的方法。 ) 。

而后经过 useMemo 建立出一个新的 contextValue ,把父级的 subscription 换成本身的 subscription。用于经过 Provider 传递新的 context这里简单介绍一下,运用了 Provider 能够和多个消费组件有对应关系。多个 Provider 也能够嵌套使用,里层的会覆盖外层的数据。react-reduxcontext更倾向于Provider良好的传递上下文的能力。

接下来经过useReducer制造出真正触发更新的forceComponentUpdateDispatch 函数。也就是整个 state 或者是 props改变,触发组件更新的函数。 为何这么作呢?

笔者认为react-redxx这样设计缘由是但愿connect本身控制本身的更新,而且多个上下级 connect不收到影响。因此一方面经过useMemo来限制业务组件没必要要的更新,另外一方面来经过forceComponentUpdateDispatch来更新 HOC 函数,产生actualChildProps,actualChildProps 改变 ,useMemo执行,触发组件渲染。

第三步:保存信息,执行反作用钩子(最重要的部分到了)

这一步十分重要,为何这么说呢,首先先经过useRef缓存几个变量:

lastChildProps -> 保存上一次 合并过的 props 信息(通过 ownprops ,stateProps , dispatchProps 合并过的 )。 lastWrapperProps -> 保存本次上下文执行 业务组件的 propsrenderIsScheduled -> 当前组件是否处于渲染阶段。 actualChildProps -> actualChildProps 为当前真正处理事后,通过合并的 props, 组件经过 dep -> actualChildProps,来判断是否进行更新。

接下来执行两次 useEffect , 源码中不是这个样子的,我这里通过简化,第一个 useEffect 执行了 captureWrapperProps ,captureWrapperProps 是干什么的呢?

//获取包装的props 
function captureWrapperProps( lastWrapperProps, lastChildProps, renderIsScheduled, wrapperProps, actualChildProps, childPropsFromStoreUpdate, notifyNestedSubs ) {
  lastWrapperProps.current = wrapperProps  //子props 
  lastChildProps.current = actualChildProps //通过 megeprops 以后造成的 prop
  renderIsScheduled.current = false  // 当前组件渲染完成
}
复制代码

captureWrapperProps 的做用很简单,在一次组件渲染更新后,将上一次 合并前合并后props,保存起来。这么作目的是,能过在两次hoc执行渲染中,对比props stateProps是否发生变化。从而肯定是否更新 hoc,进一步更新组件。

执行第二个 useEffect 是很关键。执行subscribeUpdates 函数,subscribeUpdates 是订阅更新的主要函数,咱们一块儿来看看:

function subscribeUpdates( shouldHandleStateChanges, store, subscription, childPropsSelector, lastWrapperProps, //子props  lastChildProps, //通过 megeprops 以后造成的 prop renderIsScheduled, childPropsFromStoreUpdate, notifyNestedSubs, forceComponentUpdateDispatch ) {
  if (!shouldHandleStateChanges) return

   // 捕获值以检查此组件是否卸载以及什么时候卸载
  let didUnsubscribe = false
  let lastThrownError = null
   //store更新订阅传播到此组件时,运行此回调
  const checkForUpdates = ()=>{
      //....
  }
  subscription.onStateChange = checkForUpdates
  //开启订阅者 ,当前是被connect 包转的状况 会把 当前的 checkForceUpdate 放在存入 父元素的addNestedSub中。
  subscription.trySubscribe()
  //在第一次呈现以后从存储中提取数据,以防存储从咱们开始就改变了。
  checkForUpdates()
  /* 卸载订阅起 */
  const unsubscribeWrapper = () => {
    didUnsubscribe = true
    subscription.tryUnsubscribe()
    subscription.onStateChange = null
  }

  return unsubscribeWrapper
}

复制代码

这绝对是整个订阅更新的核心,首先声明 store 更新订阅传播到此组件时的回调函数checkForUpdates把它赋值给onStateChange,若是store中的state发生改变,那么在组件订阅了state内容以后,相关联的state改变就会触发当前组件的onStateChange,来合并获得新的props,从而触发组件更新。

而后subscription.trySubscribe()把订阅函数onStateChange绑定给父级subscription,进行了层层订阅。

最后,为了防止渲染后,store内容已经改变,因此首先执行了一次checkForUpdates。那么checkForUpdates的做用很明确了,就是检查是否派发当前组件的更新。

到这里咱们明白了,react-redux 经过 subscription 进行层层订阅。对于一层层的组件结构,总体模型图以下:

接下来咱们看一下checkForUpdates

//store更新订阅传播到此组件时,运行此回调
  const checkForUpdates = () => {
    if (didUnsubscribe) {
      //若是写在了
      return
    }
     // 获取 store 里state
    const latestStoreState = store.getState()q
    let newChildProps, error
    try {
      /* 获得最新的 props */
      newChildProps = childPropsSelector(
        latestStoreState,
        lastWrapperProps.current
      )
    } 
    //若是新的合并的 props没有更改,则此处不作任何操做-层叠订阅更新
    if (newChildProps === lastChildProps.current) { 
      if (!renderIsScheduled.current) {  
        notifyNestedSubs() /* 通知子代 subscription 触发 checkForUpdates 来检查是否须要更新。 */
      }
    } else {
      lastChildProps.current = newChildProps
      childPropsFromStoreUpdate.current = newChildProps
      renderIsScheduled.current = true
      // 此状况 可能考虑到 代码运行到这里 又发生了 props 更新 因此触发一个 reducer 来促使组件更新。
      forceComponentUpdateDispatch({
        type: 'STORE_UPDATED',
        payload: {
          error
        }
      })
    }
  }
复制代码

checkForUpdates 经过调用 childPropsSelector来造成新的props,而后判断以前的 prop 和当前新的 prop 是否相等。若是相等,证实没有发生变化,无须更新当前组件,那么经过调用notifyNestedSubs来通知子代容器组件,检查是否须要更新。若是不相等证实订阅的store.state发生变化,那么当即执行forceComponentUpdateDispatch来触发组件的更新。

对于层层订阅的结构,整个更新模型图以下:

总结

接下来咱们总结一下整个connect的流程。咱们仍是从订阅更新两个方向入手。

订阅流程

整个订阅的流程是,若是被connect包裹,而且具备第一个参数。首先经过context获取最近的 subscription,而后建立一个新的subscription,而且和父级的subscription创建起关联。当第一次hoc容器组件挂在完成后,在useEffect里,进行订阅,将本身的订阅函数checkForUpdates,做为回调函数,经过trySubscribethis.parentSub.addNestedSub ,加入到父级subscriptionlisteners中。由此完成整个订阅流程。

更新流程

整个更新流程是,那state改变,会触发根订阅器的store.subscribe,而后会触发listeners.notify ,也就是checkForUpdates函数,而后checkForUpdates函数首先根据mapStoretopropsmergeprops等操做,验证该组件是否发起订阅,props 是否改变,并更新,若是发生改变,那么触发useReducerforceComponentUpdateDispatch函数,来更新业务组件,若是没有发生更新,那么经过调用notifyNestedSubs,来通知当前subscriptionlisteners检查是否更新,而后尽心层层checkForUpdates,逐级向下,借此完成整个更新流程。

四 关于 useMemo 用法思考?

整个react-redux源码中,对于useMemo用法仍是蛮多的,我总结了几条,奉上🌹🌹:

1 缓存属性 / 方法

react-redux源码中,多处应用了useMemo 依赖/缓存 属性的状况。这样作的好处是只有依赖项发生改变的时候,才更新新的缓存属性/方法,好比 childPropsSelector , subscription , actualChildProps 等主要方法属性。

2 控制组件渲染,渲染节流。

react-redux源码中,经过 useMemo来控制业务组件是否渲染。经过 actualChildProps变化,来证实是否来自 **自身 props ** 或 订阅的 state 的修改,来肯定是否渲染组件。

例子🌰:

const renderedWrappedComponent = useMemo(
    () => (
        <WrappedComponent {...actualChildProps} ref={reactReduxForwardedRef} />
    ),
    [reactReduxForwardedRef, WrappedComponent, actualChildProps]
)
复制代码

五 总结

但愿这篇文章能让屏幕前的你,对react-redux的订阅和更新流程有一个新的认识。送人玫瑰,手留余香,阅读的朋友能够给笔者点赞,关注一波 ,陆续更新前端超硬核文章。

回看笔者往期高赞文章,有更多精彩内容等着你!

vue3.0源码系列

react-hooks系列

开源项目系列

相关文章
相关标签/搜索