【源码分析】react-redux

819 阅读22分钟

前言

前面学习redux、redux-thunk的源码,接下来再接再厉,来了解一下react-redux的源码。

p.s.这里研究的源码版本号是:7.2.4

作用

官方文档有这么一段话,翻译过来的意思是:redux是独立的库,它可以用于多种库或者框架中,我们将它与某种UI库(框架)结合使用的时候,比起在UI库中操作redux,我们往往会使用一种UI binding库去作为它们之间的桥梁。

Redux itself is a standalone library that can be used with any UI layer or framework, including React, Angular, Vue, Ember, and vanilla JS. Although Redux and React are commonly used together, they are independent of each other.

If you are using Redux with any kind of UI framework, you will normally use a "UI binding" library to tie Redux together with your UI framework, rather than directly interacting with the store from your UI code.

它的主要作用有:

  • react-redux还是redux官方团队维护的,当redux更新时,这个库也会跟着更新;
  • 提高了性能,通常情况下,当一个component发生改变时,整棵树都会重新渲染一遍,但是react-redux内部帮我们做了优化,提高了性能;
  • 社区强大。

源码分析

源码src/index.js文件中暴露出来的有:

export {
  Provider,
  connectAdvanced,
  ReactReduxContext,
  connect,
  batch,
  useDispatch,
  createDispatchHook,
  useSelector,
  createSelectorHook,
  useStore,
  createStoreHook,
  shallowEqual,
}

ReactReduxContext

这个源码很简单,就是使用React.createContext创建了一个全局上下文。所以react-redux本质上就是借助context,需要借助Context给嵌套的组件传递store, 还需要当store中的state更新时,让嵌套的子组件的state也更新,所以后期会context中保存storedescription,简化了redux使用。

// src/components/Context.js
import React from 'react'

export const ReactReduxContext = /*#__PURE__*/ React.createContext(null)

if (process.env.NODE_ENV !== 'production') {
  ReactReduxContext.displayName = 'ReactRedux'
}

export default ReactReduxContext

batch

这是一个变量,跟react中的批量更新有关。批量更新,按照我现阶段的接触是这么理解的:若是连续调用N次更新,react不会每次都触发视图重渲染,而是会等更新都直接完获得最终结果的时候才会更新视图。

// src/utils/reactBatchedUpdates
export { unstable_batchedUpdates } from 'react-dom'

// src/index.js
import { unstable_batchedUpdates as batch } from './utils/reactBatchedUpdates'

Provider

Provider的作用就是,你可以在父级组件中将store的状态注入,然后父组件包裹的任意组件都可以使用到store中的状态,而不用每个文件都要手动引用store

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

Provider是一个组件,那么基本结构跟普通的组件无异,根据文档可知,它接受三个参数:storechidren(一般是我们的App组件)context,(如果这样提供了context的话,还需要为所有连接的组件提供相同的context)。

上文我们知道了react-redux主要是运用到了context,这里我们不难分析出Provider的基本架构:

import { ReactReduxContext } from './Context'

function Provider({store, context, children}){
    const Context = context || ReactReduxContext;
    return (
    	<Context.Provider value={store}>{children}</Context.Provider>
    )
}

Provider中我们需要对Contextstore来做一些处理,在此之前,我们先来分析几个小方法:

createListenerCollection

从函数名字来看,就是创建一个listener收集器,主要作用就是创建链表,每个节点有callback回调函数和双指针:前一个节点的指针(prev)和下一个节点的指针(next)。

createListenerCollection内声明一个first指向这个链表的头部,last指向这个链表的尾巴,还定义了以下几个方法:

  • clear: 清除firstlast指针,即清空链表;

  • notify:遍历listener中的每个节点,调用他们的callback。这里还使用到了上面提到的batch。猜测是遍历节点调用函数,batch可以确保函数都调用完成之后再一次性更新,达到批处理的作用,而不是每执行一次回调函数,就更新一次;

  • get:遍历整个链表,将每个节点依次保存在listeners数组中,并返回;

  • subscribe: 新增监听的节点,先设置isSubscribed = true,表明已经监听,然后像链表新增节点一样新增节点:

    • 创建一个新节点,含有callback、prev和next属性, 其中prev赋值为last(因为新节点会放在最后一个节点后面嘛);
    • 判断现在的链表是不是空的,如果是空的,那么这个节点就是头节点,否则,就将当前节点的上一个节点的next指向这个节点。

    最后还会返回一个取消监听的函数,取消监听这个节点:

    • 如果isSubscribedfalse或者first(链表)为空,就直接返回;
    • 设置isSubscribedfalse
    • 删除逻辑可能有点绕,我们举个例子,当前节点是B:
      • 如果B有下一个节点C,那么我们将Cprev指向B的上一个节点;
      • 否则,即B没有下一个节点,那么说明此时last指向的是B,那么应该将last指向B的上一个节点;
      • 如果B有上一个节点A, 那么Anext应该指向B的下一个节点;
      • 否则,说明B即在链表头,此时first指向的就是B,应该修改为B的下一个节点。
import { getBatch } from './batch'

function createListenerCollection() {
  // batch是批处理,react的更新机制跟batch有关,当有多个值一起更新时,会将他们合并起来一次更新,而不是每次更新一个值就重新渲染
  const batch = getBatch()
  let first = null  // 头指针
  let last = null   // 尾指针, 说明这个时双向链表

  return {
    // 清空指针,即链表清空
    clear() {
      first = null  
      last = null
    },

    notify() {
      batch(() => {                  // 进行批处理调用了回调函数
        let listener = first         // 从头遍历链表,执行每个回调函数
        while (listener) {
          listener.callback()
          listener = listener.next
        }
      })
    },

    get() {
      let listeners = []
      let listener = first
      while (listener) {
        listeners.push(listener)
        listener = listener.next
      }
      return listeners                //  返回所有订阅的回调函数
    },

    subscribe(callback) {
      let isSubscribed = true        //  表示已订阅

      let listener = (last = {       //  listener是一个节点,含有双向指针
        callback,          
        next: null,                  // 新增的节点,尾巴是空节点,所以是null
        prev: last,                  // 新增节点的前一个节点,肯定是上一个节点
      }) 

      if (listener.prev) {  
        // 如果新增的节点有前一个节点的话,那么它上一个节点要用next指针指向它
        listener.prev.next = listener
      } else {
        // 没有的话,说明新增的节点就算第一个节点
        first = listener
      }

      return function unsubscribe() {
        // 如果没有订阅,或者链表是空的,那么就不用取消监听啦
        if (!isSubscribed || first === null) return
        // 取消订阅
        isSubscribed = false
		
        // 由于删除了该节点,需要修改它的前后节点的指针指向
        // 也许还需要修改first和last指针
        if (listener.next) {
          listener.next.prev = listener.prev
        } else {
          last = listener.prev
        }
        if (listener.prev) {
          listener.prev.next = listener.next
        } else {
          first = listener.next
        }
      }
    },
  }
}

Subscription

Providerstore中的state传入到各个组件中并在state更新的时候同时让各个组件中的store也发生改变,那么需要实时监听store并且支持解除绑定监听的操作。所以内部封装一个名为Subscription的函数,这个函数的意思是订阅。这个函数后期在其他方法实现中也会用到,所以需要理清它的思路。

encapsulates the subscription logic for connecting a component to the redux store, as well as nesting subscriptions of descendant components, so that we can ensure the ancestor components re-render before descendants

封装用于将组件连接到redux存储的订阅逻辑,以及对后代组件的嵌套订阅,以便我们能够确保祖先组件在后代组件之前re-render

// src/utils/Subscription.js
const nullListeners = { notify() {} }

export default class Subscription {
  constructor(store, parentSub) {
    this.store = store              // redux中的store
    this.parentSub = parentSub      // context.store
    this.unsubscribe = null         // 订阅对象
    this.listeners = nullListeners  // 订阅回调函数

    this.handleChangeWrapper = this.handleChangeWrapper.bind(this)
  }
  
  // 给嵌套的组件增加监听
  addNestedSub(listener) {
    this.trySubscribe()
    return this.listeners.subscribe(listener)
  }
    
  // 通知嵌套组件state发生了改变
  notifyNestedSubs() {
    this.listeners.notify()
  }
  
  // 当store改变时执行的回调函数
  handleChangeWrapper() {
    if (this.onStateChange) {
      this.onStateChange()
    }
  }
  
  //  判断是否订阅了
  isSubscribed() {
    return Boolean(this.unsubscribe)
  }
	
  // 新增订阅
  // 如果没有订阅,那么进行订阅,其中parentSub的优先级比store高
  trySubscribe() {
    if (!this.unsubscribe) {
      this.unsubscribe = this.parentSub
        ? this.parentSub.addNestedSub(this.handleChangeWrapper)
        : this.store.subscribe(this.handleChangeWrapper)
	  // 初始化listeners, 创建链表
      this.listeners = createListenerCollection()
    }
  }
  
  // 取消订阅
  tryUnsubscribe() {
    if (this.unsubscribe) {
      this.unsubscribe()
      this.unsubscribe = null
      this.listeners.clear()
      this.listeners = nullListeners
    }
  }
}

综上所述,Subscription的作用就是传入store并创建一个listeners对象,来对store进行监听并通知各个子组件,或者传入parentSub,新增监听节点,并遍历

useIsomorphicLayoutEffect

在研究useIsomorphicLayoutEffect之前,我们先来捋一下useEffectuseLayoutEffect的区别:

  • useEffect: 当render结束之后,callback函数会被执行,由于是异步的,所以不会阻塞浏览器的渲染;
  • useLayoutEffect: 如果你在useEffect中需要处理DOM操作,可以使用useLayoutEffect,不会出现闪屏的情况。它会在DOM完成后立即执行,但是会在浏览器进行任何绘制之前执行,所以会阻塞浏览器的渲染,是同步渲染;
  • 无论 useLayoutEffect还是 useEffect 都无法在 Javascript 代码加载完成之前执行。在服务端渲染组件中引入 useLayoutEffect 代码时会触发 React 警告。解决这个问题,需要将代码逻辑移至 useEffect 中。

Provider

import { useEffect, useLayoutEffect } from 'react'

export const useIsomorphicLayoutEffect =
  typeof window !== 'undefined' &&
  typeof window.document !== 'undefined' &&
  typeof window.document.createElement !== 'undefined'
    ? useLayoutEffect
    : useEffect

为了解决上面的问题,浏览器渲染使用的是useLayoutEffect,服务器端渲染使用的是useEffect。使用useLayoutEffect是为了确保store的订阅回调函数的执行发生在最后一次的render commit,否则当store的更新发生在rendereffect之间时,我们可能会丢失更新。

我们也需要确保subscription创建的时候是异步的,否则当store的更新发生在subscription的创建前,我们观察到的state可能与store中的不一致。

源码

import React, { useMemo } from 'react'
import PropTypes from 'prop-types'
import { ReactReduxContext } from './Context'
import Subscription from '../utils/Subscription'
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'

function Provider({ store, context, children }) {
  // useMemo会在依赖项发生改变的时候,重新调用内部的函数
  // 所以当store发生改变的时候,会重新计算contextValue的值
  const contextValue = useMemo(() => {
    // 创建Subscription对象
    const subscription = new Subscription(store)
    // 将notifyNestedSubs绑定在onStateChange
    subscription.onStateChange = subscription.notifyNestedSubs
      
    // 敲重点,在react-redux中,contextValue的值都含有这两个属性
    // subscription是一个监听对象,当store发生改变时,可以通过subscription
    // 给订阅了store的组件传递信息、也可以取消监听
    return {
      store,
      subscription,
    }
  }, [store])
  
  // 当store发生改变的时候,重新计算当前的getState的值
  const previousState = useMemo(() => store.getState(), [store])
  
  // 当previousState或者contextValue发生改变的时候触发下面的逻辑
  useIsomorphicLayoutEffect(() => {
    const { subscription } = contextValue
    // 遍历链表的每个节点,触发回调函数,给订阅了store的组件发布通知
    // 之后创建一个新的链表
    subscription.trySubscribe()
    
    // 可能发布通知之后state的值发生改变,
    // 继续遍历链表,通知
    if (previousState !== store.getState()) {
      subscription.notifyNestedSubs()
    }
      
    return () => {
      // 取消监听,便于垃圾回收机制回收
      subscription.tryUnsubscribe()
      subscription.onStateChange = null
    }
  }, [contextValue, previousState])
  
  // 如果我们没传递context,就默认使用ReactReduxContext
  const Context = context || ReactReduxContext

  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}

export default Provider

从源码可以看出来Provider是一个函数,即可以充当组件,内部主要执行了几个步骤:

  • store创建了一个subscription对象,便于store更新的时候借助subscription进行通知,并将两个赋值给ContextValue;
  • 使用Context.ProviderContextValue传递给Children

这里可以抛出一个疑问,如果react-redux中是借助context来传递store的话,那么如果同时要支持自定义的context的话,内部做了什么处理?

当在Provider中使用Context时候,先创建一个Conetxt,将其作为参数传入Provider中,但是一定要在子组件中手动引入这个Context不然就会报错:

import React from 'react';
export const ColorContext = React.createContext({color: 'red'});
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from './store';
import { Provider } from 'react-redux';
import { ColorContext } from './store/context';

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

为了避免错误,在子组件中,context作为props传入:

// App.js
<Count context={ColorContext} />

为了在内部读取到ColorContext,可以再ownProps中获取:

// Counter.js
function Counter(props){
  const { count, increment, ColorContext } = props;
  const contextValue = useContext(ColorContext);
  console.log('contextValue123', contextValue);
    
  return (
    <>
      <p>{count}</p>
      <button onClick = { () => increment(1)}>+</button>
    </>
  )
}


function mapStateToProps(state, ownProps) {
  return { 
    count: state.count,
    ColorContext:  ownProps.ownProps
  }
}

function mapDispatchToProps(dispatch) {
  return { increment: bindActionCreators(actionCreators.increment, dispatch) }
}

export default connect(mapStateToProps, mapDispatchToProps)(Counter);

另外根据后面的源码可得, 这样也不会报错...,但是无法在ownProps获得Context

 <Count store={store} />

connectAdvanced

这是一个从来没有用过的API!connect的底层是靠它实现的,不过官方文档说,未来可能会将它从代码中移除。connectAdvanced的跟connect的区别在于,并没有规定如何将state、props和dispatch传入到最终的props,也没有对产生的props做缓存来优化性能。

connectAdvanced接收两个参数:selectorFactoryfactoryOptions。在selectorFactory我们可以实现将什么样的参数传入到组件中,这些参数可以依赖于store.getState()action和组件自身的props。官方DEMO:

function selectorFactory(dispatch) {
  let ownProps = {}
  let result = {}

  const actions = bindActionCreators(actionCreators, dispatch)
  const addTodo = (text) => actions.addTodo(ownProps.userId, text)

  return (nextState, nextOwnProps) => {
    const todos = nextState.todos[nextOwnProps.userId]
    const nextResult = { ...nextOwnProps, todos, addTodo }
    ownProps = nextOwnProps
    if (!shallowEqual(result, nextResult)) result = nextResult
    return result
  }
}
export default connectAdvanced(selectorFactory)(TodoApp)

我们姑且将selectorFactory中返回的函数叫做selectorselector函数接收两个参数,新的state和组件自身的propsselector函数会在组件接收到新的state或者props时被调用,然后返回一个简单对象给被包裹的组件。

factoryOptions中的参数可以有这几种:

  • getDisplayName: 获得被connectAdvanced包裹后返回的新组件的名字,默认是ConnectAdvanced('+name+')
  • methodName: 用来显示在错误信息中的,默认是connectAdvanced;
  • shouldHandleStateChanges: 是否要跟踪state的变化,默认值是true
  • forwardRef: 当需要使用ref来保存被connectAdvanced包裹之后的新组件时,要设置为false;
  • contextcreateContext创建的context
  • 其余参数: 会被传递给selectorFactoryselector方法的第二个参数中。

这里上一下源码的基本架构,有些即将要被移除的参数被我删掉了!

export default function connectAdvanced(
	selectorFactory,
    {
    	getDisplayName = (name) => `ConnectAdvanced(${name})`,
    	methodName = 'connectAdvanced',
        shouldHandleStateChanges = true,
    	forwardRef = false,
        context = ReactReduxContext,
        // 剩余的参数会传递给selectorFactory
    	...connectOptions
	}
){	
    // ...
    return function wrapWithConnect(WrappedComponent) {
        // ...
        return hoistStatics(Connect, WrappedComponent)
    }
}

hoistStatics来源于hoist-non-react-statics这个包,它可以将你原先组件中的静态属性复制给connectAdvanced包裹后的新组件。举个栗子:

const Component.staticMethod = () => {}
const WrapperComponent = enhance(Component);
typeof WrapperComponent.staticMethod === undefined // true

// 解决方法如下
function enhance(Component) {
  class WrapperComponent extends React.Component {/*...*/}
  WrapperComponent.staticMethod = Component.staticMethod;
  return Enhance;
}

// 当时若我们不知道原组件的所有静态属性值,上面的方法便无效了,
// 所以可以借助hoist-non-react-statics
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(Component) {
  Class WrapperComponent extends React.Component {/* ... */}
  hoistNonReactStatic(WrapperComponent, Component);
  return WrapperComponent;
}

继续分析,来理解一下内部的每行代码:

export default function connectAdvanced(){
    // 这里的前半部分,都对即将移除的属性做报错...
    
    const Context = context;   // 保存要传递到组件内部的context
    return function wrapWithConnect(WrappedComponent) {
        // ...
    }
}

进入到wrapWithConnect源码:

function wrapWithConnect(WrappedComponent) {
   // 先获得组件的名字
    const wrappedComponentName =
      WrappedComponent.displayName || WrappedComponent.name || 'Component'
     // 给被包裹之后返回的新组件的起名字 (name) => `ConnectAdvanced(${name})`
     const displayName = getDisplayName(wrappedComponentName)
     
     const selectorFactoryOptions = {
      ...connectOptions,
      getDisplayName,
      methodName,
      shouldHandleStateChanges,
      displayName,
      wrappedComponentName,
      WrappedComponent,
    }
    
     const { pure } = connectOptions
     
     // 创建我们的selector函数
     function createChildSelector(store) {
      return selectorFactory(store.dispatch, selectorFactoryOptions)
    }
     
     // 根据pure来判断是否用useMemo来提高被包裹返回的新组件的性能
     const usePureOnlyMemo = pure ? useMemo : (callback) => callback()
     
     function ConnectFunction(props){}
     
     // 根据pure来判断是否用React.memo来提高被包裹返回的新组件的性能
     const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction
     Connect.WrappedComponent = WrappedComponent
     Connect.displayName = ConnectFunction.displayName = displayName
    
    // ...
}

这里有一个ConnectFunction函数,源码巨长,继续分析:

function ConnectFunction(props) {
    // 从props中解构出了context、ref和剩余的Props
    const [
        propsContext,
        reactReduxForwardedRef,
        wrapperProps,
      ] = useMemo(() => {
        const { reactReduxForwardedRef, ...wrapperProps } = props
        return [props.context, reactReduxForwardedRef, wrapperProps]
      }, [props])
     
     // 之前有 context = ReactReduxContext; const Context = context
     // Provider中用户可以传入自定义的Context,如果没有就默认是ReactReduxContext
     // 用户如果之前在Provider传入context,需要在嵌套组件中手动传入该Context,故在这里我们需要对Context的值进行判断,
    // 使用合适的Context
     const ContextToUse = useMemo(() => {
        return propsContext &&
          propsContext.Consumer &&
          isContextConsumer(<propsContext.Consumer />)
          ? propsContext
          : Context
      }, [propsContext, Context])
     
     // 获得上下文中的值
     const contextValue = useContext(ContextToUse)
     
     // 前面讲Provider的时候提到过了
     // Provider中封装了contextValue, 内部含有store和subscription
     // 如果用户传了自定义的Context,嵌套组件必须传通过Context或者是store才不会报错。
     
     // 这里要判断contextValue.store的原因就是
     // 如果没有store,则需要从contextValue中找store,如果contextValue中也没有
     // 就会报错
     // 所以这就是为什么如果用户传递了context, 那么需要在嵌套组件中自定义传入context或者store的原因
     const didStoreComeFromProps =
        Boolean(props.store) &&
        Boolean(props.store.getState) &&
        Boolean(props.store.dispatch)
      const didStoreComeFromContext =
        Boolean(contextValue) && Boolean(contextValue.store)

      if (
        process.env.NODE_ENV !== 'production' &&
        !didStoreComeFromProps &&
        !didStoreComeFromContext
      ) {
        throw new Error(
          `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.`
        )
      }
    
    // 保存store
    const store = didStoreComeFromProps ? props.store : contextValue.store
    
    // 前面提到了,如果store发生改变,那么会调用selector函数
    const childPropsSelector = useMemo(() => {
    	return createChildSelector(store)
  	}, [store])
    
    // ...
}
const [subscription, notifyNestedSubs] = useMemo(() => {
    // 如果不需要监听state的变化,则返回[null, null]
    if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY

    // 如果store是由context提供的话,需要对contextValue.subscription进行监听
    const subscription = new Subscription(
      store,
      didStoreComeFromProps ? null : contextValue.subscription
    )

    // 复制notifyNestedSubs
    const notifyNestedSubs = subscription.notifyNestedSubs.bind(
      subscription
    )

    return [subscription, notifyNestedSubs]
}, [store, didStoreComeFromProps, contextValue])

// 覆盖当前的contextValue
const overriddenContextValue = useMemo(() => {
    // 如果store是来自Props,那么直接返回contextValue
    if (didStoreComeFromProps) {
      return contextValue
    }

    // 否则,将该组件的subscription放入上下文中
    return {
      ...contextValue,
      subscription,
    }
}, [didStoreComeFromProps, contextValue, subscription])

// 当store更新时,需要强制更新被被包裹的组件也进行更新,进而对子组件也进行更新
const [
    [previousStateUpdateResult],
    forceComponentUpdateDispatch,
] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)

// 当发生错误的时候会抛出异常,正常情况的话previousStateUpdateResult应该是null
if (previousStateUpdateResult && previousStateUpdateResult.error) {
	throw previousStateUpdateResult.error
}

// 这里贴一下各个参数的源码
function storeStateUpdatesReducer(state, action) {
  const [, updateCount] = state
  return [action.payload, updateCount + 1]
}

const EMPTY_ARRAY = []
const initStateUpdates = () => [null, 0]
// 前面提到过,useIsomorphicLayoutEffect内部判断,
// 如果是服务端返回useEffect
// 如果是浏览器端返回useLayoutEffect
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'

// 这里可以理解成,调用了useEffect/useLayoutEffect,不过参数都是动态的
function useIsomorphicLayoutEffectWithArgs(
  effectFunc,
  effectArgs,
  dependencies
) {
  useIsomorphicLayoutEffect(() => effectFunc(...effectArgs), dependencies)
}

// 继续分析源码
const lastChildProps = useRef()   // 更新之前传入当前组件的props(Component)
const lastWrapperProps = useRef(wrapperProps) // 更新之前新组件的props (ConnectComponent)
const childPropsFromStoreUpdate = useRef()     // 判断组件的更新是否是因为store的更新
const renderIsScheduled = useRef(false)

// usePureOnlyMemo是根据pure来判断是否使用memo
const actualChildProps = usePureOnlyMemo(() => {
// 视图更新可能是由于store更新导致的会让组件	(Component)获得新的props
// 如果Component有新的props,而ConnectComponent的props不变,那么应该使用新的props, 这样我们可以获得ConnectComponent的新props
// 但是如果我们有了新的ConnectComponent props,它可能会改变Component的props,那么就会重新进行计算
// 为了避免出现问题,只有当ConnectComponent的props不变时,我们才会根据Component的新props进行更新
if (
  childPropsFromStoreUpdate.current &&
  wrapperProps === lastWrapperProps.current
) {
  return childPropsFromStoreUpdate.current
}

// 调用selector函数
return childPropsSelector(store.getState(), wrapperProps)
}, [store, previousStateUpdateResult, wrapperProps])
      
// 我们需要下面的函数来执行同步到渲染,但是在SSR中使用useLayoutEffect会报错,这里我们需要检测一下,如果是ssr,那么就改为使用effect来避免这个警告
useIsomorphicLayoutEffectWithArgs(captureWrapperProps, [
    lastWrapperProps,
    lastChildProps,
    renderIsScheduled,
    wrapperProps,
    actualChildProps,
    childPropsFromStoreUpdate,
    notifyNestedSubs,
])

function captureWrapperProps(
  lastWrapperProps,
  lastChildProps,
  renderIsScheduled,
  wrapperProps,
  actualChildProps,
  childPropsFromStoreUpdate,
  notifyNestedSubs
) {
  // 为了方便下一次的比较
  lastWrapperProps.current = wrapperProps
  lastChildProps.current = actualChildProps
  renderIsScheduled.current = false

  // 清空引用并进行更新
  if (childPropsFromStoreUpdate.current) {
    childPropsFromStoreUpdate.current = null
    notifyNestedSubs()
  }
}

// 当store或者subscription发生改变时,我们重新订阅
useIsomorphicLayoutEffectWithArgs(
    subscribeUpdates,
    [
      shouldHandleStateChanges,
      store,
      subscription,
      childPropsSelector,
      lastWrapperProps,
      lastChildProps,
      renderIsScheduled,
      childPropsFromStoreUpdate,
      notifyNestedSubs,
      forceComponentUpdateDispatch,
    ],
[store, subscription, childPropsSelector]
)

function subscribeUpdates(
  shouldHandleStateChanges, // 是否对store的更新进行订阅
  store,                    // store
  subscription,             // subscription
  childPropsSelector,       // selector
  lastWrapperProps,         // 上一次组件的props
  lastChildProps,           // 上一次的子props
  renderIsScheduled,        // 是否正在调度
  childPropsFromStoreUpdate //判断子props是否是来自store的更新 
  notifyNestedSubs,         // notifyNestedSubs
  forceComponentUpdateDispatch
) {
  // 如果没有订阅,直接返回
  if (!shouldHandleStateChanges) return

  // 捕获值,并检查是否以及何时卸载该组件
  let didUnsubscribe = false
  let lastThrownError = null

  // 当每次store更新传播到这个组件时,我们就会调用这个函数
  const checkForUpdates = () => {
    if (didUnsubscribe) {
      // 如果已经取消订阅,直接返回
      return
    }
	// 获得更新前的store
    const latestStoreState = store.getState()

    let newChildProps, error
    try {
      // 调用selector来获得最新的子props
      newChildProps = childPropsSelector(
        latestStoreState,
        lastWrapperProps.current
      )
    } catch (e) {
      error = e
      lastThrownError = e
    }

    if (!error) {
      lastThrownError = null
    }

    // 如果新旧props一样,那么就不做任何处理
    if (newChildProps === lastChildProps.current) {
      if (!renderIsScheduled.current) {
        // 调用subscription.notifyNestedSubs()
        notifyNestedSubs()
      }
    } else {
      // 用ref来存储最新的props,如果使用useState/useReducer来追踪,
      // 那么将无法确定这个值是否会加工过
      // 也无法清空这个值触发进行强制渲染
      lastChildProps.current = newChildProps
      childPropsFromStoreUpdate.current = newChildProps
      renderIsScheduled.current = true

      // 如果子props被更新了,那么可以重新渲染了
      forceComponentUpdateDispatch({
        type: 'STORE_UPDATED',
        payload: {
          error,
        },
      })
    }
  }

  subscription.onStateChange = checkForUpdates
  subscription.trySubscribe()

  // 将store中发生改变的值进行视图更新
  checkForUpdates()

  const unsubscribeWrapper = () => {
    didUnsubscribe = true
    subscription.tryUnsubscribe()
    subscription.onStateChange = null

    if (lastThrownError) {
      // 当store更新,但是某个组件没有依赖store中更新的state切mapState方法有问题时
      // 就会报错
      throw lastThrownError
    }
  }

  return unsubscribeWrapper
}
// 回到主要源码中,我们已经处理完了需要传递的参数,接下来就是渲染组件
// 还使用了useMemo来优化性能
const renderedWrappedComponent = useMemo(
() => (
  <WrappedComponent
    {...actualChildProps}
    ref={reactReduxForwardedRef}
  />
),
[reactReduxForwardedRef, WrappedComponent, actualChildProps]
)

// 如果React看到与上次完全相同的元素引用,它会退出重新呈现子元素,就像它被包装在React.memo()中或从shouldComponentUpdate返回false一样。
const renderedChild = useMemo(() => {
if (shouldHandleStateChanges) {
  // 如果需要根据store的变化更新组件,那么就使用context.provider封装组件
  // 并传入处理好的值
  return (
    <ContextToUse.Provider value={overriddenContextValue}>
      {renderedWrappedComponent}
    </ContextToUse.Provider>
  )
}

// 否则直接返回原先的组件
return renderedWrappedComponent
}, [ContextToUse, renderedWrappedComponent, overriddenContextValue])

return renderedChild

到这里connectAdvanced的源码就分析完毕了。我们来捋一下思路,为了方便说明我先写行代码:

function selector = (dispatch, ...args) => {}
const connectComponent = connectAdvanced(selector, options)(component)
  • connectAdvanced是一个函数,接收一个selector方法和options对象:

    • selector方法是用户自定义的,接收一个dispacth参数,最后需要返回一个含有入参为nextState和nextOwnProps的函数,该内部函数最后会返回一个对象,对象的键值对就是即将传给component组件的props
    • options对象,可以传递官方指定的,也可以自定义,如果是自定义的,最后会被当做传入selector的第二个参数中。官方指定的参数中,有几个比较重要,其中pure的值确定了最后返回的组件是否要进行性能优化;
    • connectAdvanced最后返回的是一个高阶函数,参数就是要包裹的组件。
  • 接下来对options中的值进行校验,并且对一些即将移除的属性做出警告处理;

  • 保存options中的context

  • 返回高阶函数wrapWithConnect(WrappedComponent),参数就是要包裹的组件,接下来就是分析wrapWithConnect(WrappedComponent)中的内容了;

  • 先对传入的组件进行校验,判断是不是一个合格的组件;

  • 记录被包裹的组件component的名字,进而给返回的新组件connectComponent起名字;

  • options中多余的参数和定义好的参数封装起来,作为selector的参数;

  • 根据options的参数pure,用usePureOnlyMemo变量来判断是否使用useMemo方法;

  • 使用ConnectFunction创建一个Connect组件,还给这个组件设置了WrappedComponentdisplayName,其中这个ConnectFunctionconnectAdvanced中的核心:

    • ConnectFunction接受一个参数props, 这个props就是被包裹后的ConnetComponent的参数props
    • context、ref和剩余的参数props中解耦出来;
    • 获得最终的context,并从中取得contextValue
    • 获得store;
    • 定义一个memo函数,当store的值发生改变,就调用用户自定义的selector函数;
    • 获得最终的ContextValue的值;
    • 获得最终要返回被包裹的组件的actualChildProps
    • 当依赖的值发生改变时,使用useEffect或者useLayoutEffect来调用captureWrapperProps,里面将当前的connentComponentpropsstorestate记录下来,方便下次渲染的时候比较,如果store的值发生更新,调用notifyNestedSubs触发每个回调函数;
    • store或者subscription进行更新时,发起通知,subscribeUpdates
    • 最后判断shouldHandleStateChanges,来决定是否在外面包裹Context,不需要则直接返回Connect组件。
  • 获得Connect组件后,通过判断forwardRef,如果存在,我们需要使用React.forwardRef来转发Connect,否则直接使用Connect,还使用了hoistStatics来合并包裹前后的组件的静态属性。

小结

connectAdvanced最后的结果是返回一个Context.Comsumer,其中需要对最后传入组件的props参数进行处理,connectAdvancedconnect最大的区别在于,内部的pure属性默认是false,即它不会启动性能优化。

最后回到之前抛出的疑问:如果react-redux中是借助context来传递store的话,那么如果同时要支持自定义的context的话,内部做了什么处理?

react-redux通过Context来保存storesubscriptionsubscription用来进行订阅发布store的,当需要结合react-redux搭配使用context时,记得再嵌套组件中将同个名称的context当作props传入组件中,这样connectAdvanced会将用户自定义的Context当作props合并到最终组件的props中。

所以虽然react-redux支持你可以全局搞一个context但是没必要,很鸡肋~

shallowEqual

在研究connect源码前先学习一下shallowEqual

===is(x,y)
NaN、NaNfalsetrue
0、+0truetrue
0、-0truefalse
+0、-0truefalse
// Object.is可以对基本数据类型:null,undefined,number,string,boolean做出非常精确的比较,
// 但是对于引用数据类型是没办法直接比较的。
function is(x, y) {
  if (x === y) {
    // 处理0、+0和-0的情况
    return x !== 0 || y !== 0 || 1 / x === 1 / y
  } else {
    // 处理NaN的情况
    return x !== x && y !== y
  }
}

export default function shallowEqual(objA, objB) {
  // 先用is来判断两个变量是否相等
  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 (
      // Object.prototype.hasOwnProperty 和 in 运算符不同,该方法会忽略掉那些从原型链上继承到的属性。
      !Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false
    }
  }

  return true
}

一开始先比较,调用了is来判断两个值是否一致,这里兼容了一些常见的基本来信===判断有问题的情况;之后判断是否不是对象或者是null,如果满足就返回false, 否则就遍历两个对象的key进行比较。从源码可以看出浅比较是不适用于嵌套类型的比较的

以上就是浅比较的思想啦~

从MDN可以看出isObject.is()的一种Polyfill实现。

在这里插入图片描述

connect

看源码的时候顺便去看了官方文档,第一个感觉是我的英语真的进步了!第二个感觉是我以前是不是没翻过,怎么看到了很多疏忽的地方??!

基本架构

// src/connect/connect.js
import connectAdvanced from '../components/connectAdvanced'
import shallowEqual from '../utils/shallowEqual'
import defaultMapDispatchToPropsFactories from './mapDispatchToProps'
import defaultMapStateToPropsFactories from './mapStateToProps'
import defaultMergePropsFactories from './mergeProps'
import defaultSelectorFactory from './selectorFactory'

export function createConnect({
  onnectHOC = 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
    } = {}
   ){
       // ...
       return connectHOC()
   } 
}

export default /*#__PURE__*/ createConnect()

导出前执行了createConnect方法,最后返回的是一个我们认识的connect函数,内部调用了HOC函数。这里面涉及到了秘密密麻麻的名字相似的参数,看的有点脑壳疼。

调用createConnect()时未传递参数,那么里面的参数都是读取默认值:

connectHOC = connectAdvanced,
mapStateToPropsFactories = defaultMapStateToPropsFactories,
mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
mergePropsFactories = defaultMergePropsFactories,
selectorFactory = defaultSelectorFactory,

然后返回了我们属性的connect函数,它支持四个参数:mapStateToProps、mapDispatch、mergeProps和一个对象。由官方文档可知:

  • mapStateToProps: 函数类型,可以传入store.getState()和当前组件自身的props,最后需要返回一个对象;

  • mapDispacthToProps:函数类型,接收store.dispacth和当前组件自身的props,最后需要返回一个对象;

  • mergeProps: 函数类型,接收store.getState()store.dispacth和组件自身的props,最后需要返回对象;

  • options: 这个对象里面有几个值:

    {
      context?: Object,    // 当我们在Provider传入context时,在子组件中引入,就可以写在这里
      pure?: boolean,      // 为了提高react-redux的性能,默认开启pure,类似pureComponent,只有在组件自身的props、mapStateToProps的结果、或者是mapDispatchToProps的结果改变时才会重新渲染组件
      areStatesEqual?: Function,   // 开启pure时,传递进来的state更新时的比较函数
      areOwnPropsEqual?: Function, // 开启pure时,自身props更新时的比较函数
      areStatePropsEqual?: Function,  // 开启pure时,mapStateToProps更新时的比较函数
      areMergedPropsEqual?: Function, // 开启pure时,mergeProps的返回值更新时的比较函数
      forwardRef?: boolean,   // 如果组件需要接收ref,需要在这里设置为true
    }
    

最后返回一个高阶函数,connectHOC就是我们之前介绍的connectAdvanced,之前说的connectAdvanced接收两个参数,一个是selector函数,一个是options对象:

return connectHOC(selectorFactory, {
    // 为了在报错的时候使用
    methodName: 'connect',

    // 根据被connect包裹的组件的名字,给返回的新组件起名字
    getDisplayName: (name) => `Connect(${name})`,

    // 如果mapStateToProps为Null,那么就不需要监听更新state啦
    shouldHandleStateChanges: Boolean(mapStateToProps),

    // 下面这些值是给selectorFactory做为参数的
    // 之前说的connectAdvanced的第二个参数options是个对象
    // 里面除了规定的key,其他属性最终都会成为selector函数的第二个参数
    initMapStateToProps,
    initMapDispatchToProps,
    initMergeProps,
    pure,
    areStatesEqual,
    areOwnPropsEqual,
    areStatePropsEqual,
    areMergedPropsEqual,

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

逐步认识一下createConnect中的默认参数:

connectHoc

它的默认值是connectAdvanced,这个上面已经了解过了,就不重复。

defaultMapStateToPropsFactories

在研究之前来看一下这个源码中引入的文件wrapMapToProps

import verifyPlainObject from '../utils/verifyPlainObject'

export function wrapMapToPropsConstant(getConstant) {
  return function initConstantSelector(dispatch, options) {
    const constant = getConstant(dispatch, options)

    function constantSelector() {
      return constant
    }
    // dependsOnOwnProps是判断当前的mapStateToProps的值,有没有依赖于ownProps
    constantSelector.dependsOnOwnProps = false
    return constantSelector
  }
}

// 有没有依赖于ownProps
export function getDependsOnOwnProps(mapToProps) {
  return mapToProps.dependsOnOwnProps !== null &&
    mapToProps.dependsOnOwnProps !== undefined
    ? Boolean(mapToProps.dependsOnOwnProps)
    : mapToProps.length !== 1
}

// 封装一个mapToProps代理函数
// 检测selectorFactory是否根据ownProps来更新。
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)
    }

    // 允许detectFactoryAndVerify得到自己的props
    proxy.dependsOnOwnProps = true

    proxy.mapToProps = function detectFactoryAndVerify(
      stateOrDispatch,
      ownProps
    ) {
      proxy.mapToProps = mapToProps
      proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps)
      let props = proxy(stateOrDispatch, ownProps)
      
	  // props返回函数,则处理mapToProps,并将该新函数作为真正的mapToProps用于后续调用
      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
  }
}
// src/connect/mapStateToProps.js
import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps'

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

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

export default [whenMapStateToPropsIsFunction, whenMapStateToPropsIsMissing]

mapStateToProps返回一个数组,分别是whenMapStateToPropsIsFunctionwhenMapStateToPropsIsMissing,从函数名字可得,当mapStateToProps是函数或者是空的时候,分别调用的这两个函数。

defaultMapDispatchToPropsFactories

// src/connect/mapDispatchToProps.js
import bindActionCreators from '../utils/bindActionCreators'
import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps'

export function whenMapDispatchToPropsIsFunction(mapDispatchToProps) {
  return typeof mapDispatchToProps === 'function'
    ? wrapMapToPropsFunc(mapDispatchToProps, 'mapDispatchToProps')
    : undefined
}

export function whenMapDispatchToPropsIsMissing(mapDispatchToProps) {
  return !mapDispatchToProps
    ? wrapMapToPropsConstant((dispatch) => ({ dispatch }))
    : undefined
}

export function whenMapDispatchToPropsIsObject(mapDispatchToProps) {
  return mapDispatchToProps && typeof mapDispatchToProps === 'object'
    ? wrapMapToPropsConstant((dispatch) =>
        bindActionCreators(mapDispatchToProps, dispatch)
      )
    : undefined
}

export default [
  whenMapDispatchToPropsIsFunction,
  whenMapDispatchToPropsIsMissing,
  whenMapDispatchToPropsIsObject,
]

mapDispatchToProps返回一个数组,分别是whenMapDispatchToPropsIsFunctionwhenMapDispatchToPropsIsMissingwhenMapDispatchToPropsIsObject,从函数名字可得,当mapDispatchToProps是函数、空或者是对象的时候的时候,分别调用的这三个函数。

defaultMergePropsFactories

// src/connect/mergeProps.js
import verifyPlainObject from '../utils/verifyPlainObject'

export function defaultMergeProps(stateProps, dispatchProps, ownProps) {
  return { ...ownProps, ...stateProps, ...dispatchProps }
}

export function wrapMergePropsFunc(mergeProps) {
  return function initMergePropsProxy(
    dispatch,
    { displayName, pure, areMergedPropsEqual }
  ) {
    let hasRunOnce = false
    let mergedProps

    return function mergePropsProxy(stateProps, dispatchProps, ownProps) {
      const nextMergedProps = mergeProps(stateProps, dispatchProps, ownProps)

      if (hasRunOnce) {
        if (!pure || !areMergedPropsEqual(nextMergedProps, mergedProps))
          mergedProps = nextMergedProps
      } else {
        hasRunOnce = true
        mergedProps = nextMergedProps

        if (process.env.NODE_ENV !== 'production')
          verifyPlainObject(mergedProps, displayName, 'mergeProps')
      }

      return mergedProps
    }
  }
}

export function whenMergePropsIsFunction(mergeProps) {
  return typeof mergeProps === 'function'
    ? wrapMergePropsFunc(mergeProps)
    : undefined
}

export function whenMergePropsIsOmitted(mergeProps) {
  return !mergeProps ? () => defaultMergeProps : undefined
}

export default [whenMergePropsIsFunction, whenMergePropsIsOmitted]

mergeProps返回一个数组,分别是whenMergePropsIsFunctionwhenMergePropsIsOmitted,从函数名字可得,当mergeProps是函数或者是省略了的时候,分别调用的这两个函数。

defaultSelectorFactory

这个函数的名字翻译过来是: 选择工厂函数,就生成connectAdvanced需要的selector函数,selector的第一个参数是dispatch, 第二个参数是对象,来瞄一下源码:

// import defaultSelectorFactory from './selectorFactory'
// selectorFactory.js

export default function finalPropsSelectorFactory(
  dispatch,
  { initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options }
) {
  // 先初始化值
  const mapStateToProps = initMapStateToProps(dispatch, options)
  const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
  const mergeProps = initMergeProps(dispatch, options)

  if (process.env.NODE_ENV !== 'production') {
    // ...
  }
    
  // 根据pure使用不同的函数,并调用它们
  const selectorFactory = options.pure
    ? pureFinalPropsSelectorFactory
    : impureFinalPropsSelectorFactory

  return selectorFactory(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    dispatch,
    options
  )
}
impureFinalPropsSelectorFactory

如果我们没有开启pure的话,那么不做什么骚操作,直接返回mergeProps,输出最后要传递给包裹后的组件的props

export function impureFinalPropsSelectorFactory(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  dispatch
) {
  return function impureFinalPropsSelector(state, ownProps) {
    return mergeProps(
      mapStateToProps(state, ownProps),
      mapDispatchToProps(dispatch, ownProps),
      ownProps
    )
  }
}
pureFinalPropsSelectorFactory

当第一次执行这个函数的时候,会使用存储这次的state、props,再通过这两个值,去获得最后传给包裹组件的stateprops合并后的props

如果已经执行过的话,调用handleSubsequentCalls来对比此次更新是只更新state、只更新props还是stateprops都更新,进而调用不同的逻辑,最后给包裹的组件最终的props

敲黑板了,下面的逻辑跟react-redux的性能有关。我们在使用connect是可以传入mapStateToProps、mapDispatchtoProps、mergeProps和Options四个参数,其中前两个是必传的。

  • mapStateToProps可以传入两个参数,stateownProps,最后返回一个新的state,所以我们可以知道新的state不仅取决于store的变化,也取决于ownProps的变化,他们两个只要更新一个那么就会重新计算state
  • mapDispatchtoProps也支持传入两个参数,dispatchownProps,返回一个新的dispatch,新的dispatch的更新取决于dispacthownProps
  • mergeProps是一个函数,默认情况下我们都会省略,所以会被默认赋值,最后将新的state、新的dispacthownProps变为新的props传递给Connect组件。

所以不管是store的更新还是组件自身props的更新,都会影响到被connect返回的新组件,这种性能开销是很大的,所以react-redux做了性能优化。

connectconnectAdvanced多了一个pure的参数,默认是true。是用来开启性能优化的。

export function pureFinalPropsSelectorFactory(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  dispatch,
  { areStatesEqual, areOwnPropsEqual, areStatePropsEqual }
) {
  let hasRunAtLeastOnce = false
  let state
  let ownProps
  let stateProps
  let dispatchProps
  let mergedProps
	
  // 首次执行,根据当前的state和props,获得最终的stateProps和dispatchProps
  // 进而同个mergedProps获得最终传入组件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
  }
  
  // 如果自身的Props和store都改变,调用这个函数
  function handleNewPropsAndNewState() {
    // 那么直接通过mapStateToProps获得新的stateProps
    stateProps = mapStateToProps(state, ownProps)
    // mapDispatchToProps方法如果有依赖ownProps,那么也需要重新获得新的dispatchProps
    if (mapDispatchToProps.dependsOnOwnProps)
      dispatchProps = mapDispatchToProps(dispatch, ownProps)
	// 传递新的stateProps、dispatchProps,通过mergedProps获得最终传入组件的props
    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
    return mergedProps
  }
	
  // 如果只有自身的props改变
  function handleNewProps() {
    // 声明mapStateToProps时如果使用了ownProps参数同样会产生新的stateProps!
    if (mapStateToProps.dependsOnOwnProps)
      stateProps = mapStateToProps(state, ownProps)
	
    // 声明mapDispatchToProps时如果使用了第二个参数(ownProps)这里会标记为true
    if (mapDispatchToProps.dependsOnOwnProps)
      dispatchProps = mapDispatchToProps(dispatch, ownProps)

    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
    return mergedProps
  }
 
  // 只有store改变
  function handleNewState() {
    // 当store改变,那么mapStateToProps会返回新的值
    const nextStateProps = mapStateToProps(state, ownProps)
    // 判断新的stateProps和上一次的stateProps是否相等
    const statePropsChanged = !areStatePropsEqual(nextStateProps, stateProps)
    stateProps = nextStateProps
 	
    // 如果改变,才需要重新计算最终的props
    if (statePropsChanged)
      mergedProps = mergeProps(stateProps, dispatchProps, ownProps)

    return mergedProps
  }
 
  // 如果不是第一次执行,
  function handleSubsequentCalls(nextState, nextOwnProps) {
    // 判断组件自身的Props是否改变
    const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
    // 判断store是否改变
    const stateChanged = !areStatesEqual(nextState, state)
    // 记录下当前的state和ownProps,方便下次更新比较
    state = nextState
    ownProps = nextOwnProps

    if (propsChanged && stateChanged) return handleNewPropsAndNewState()
    if (propsChanged) return handleNewProps()
    if (stateChanged) return handleNewState()
    return mergedProps
  }

  return function pureFinalPropsSelector(nextState, nextOwnProps) {
    // 判断是否执行过一次,来调用handleSubsequentCalls或者handleFirstCall
    return hasRunAtLeastOnce
      ? handleSubsequentCalls(nextState, nextOwnProps)
      : handleFirstCall(nextState, nextOwnProps)
  }
}

捋清楚selectorFactory,接下来就是connectAdvanced的执行流程了,就不复述了。

Hook

最后来聊一聊新增的Hook,6月份项目还在啃老本,明明项目用的是hook,但是关于react-redux却完全不知道有hook相关的api存在,继续学习:

useSelector

useSelector有点类似于mapStateToProps,可以从store中取得我们想要的state,但是不同的是mapStateToProps用的是浅比较,而useSelector用的是深比较,只要state发生改变,就肯定会触发重新渲染,所以一般情况下,useSelector只返回一个值,如果想要返回一个对象,建议搭配shallowEqual进行使用,或者借助reselect创建一个记忆选择器,该选择器在一个对象中返回多个值,但只在其中一个值发生变化时返回一个新对象。

import { shallowEqual, useSelector } from 'react-redux'
import { createSelector } from 'reselect'

// 常规用法
export const CounterComponent = () => {
  const counter = useSelector((state) => state.counter)
  return <div>{counter}</div>
}

// 使用浅层比较,获得一个对象
export const ObjComponent = () => {
  const obj = useSelector((state) => state.obj, shallowEqual)
  return <div>{obj.counter}</div>
}

// 借助reselect
const selectNumCompletedTodos = createSelector(
  (state) => state.todos,
  (todos) => todos.filter((todo) => todo.completed).length
)

export const CompletedTodosCounter = () => {
  const numCompletedTodos = useSelector(selectNumCompletedTodos)
  return <div>{numCompletedTodos}</div>
}

export const App = () => {
  return (
    <>
      <span>Number of completed todos:</span>
      <CompletedTodosCounter />
    </>
  )
}

接下来开始分析源码:

// useSelector是由createSelectorHook返回的
export const useSelector = /*#__PURE__*/ createSelectorHook()

所以可以推测出

createSelectorHook() = (selector, equalityFn) => selectedState

来看看createSelectorHook:

export function createSelectorHook(context = ReactReduxContext) {
  // 前面createSelectorHook()没有传递参数,所以context = ReactReduxContext
  const useReduxContext =
    // 获得context
    context === ReactReduxContext
      ? useDefaultReduxContext
      : () => useContext(context)
  
  // 返回一个(selector, equalityFn = refEquality) => selectorState的函数给useSelector
  // const refEquality = (a, b) => a === b useSelector默认是严格比较
  return function useSelector(selector, equalityFn = refEquality) {
    // 对selector, equalityFn进行校验
    if (process.env.NODE_ENV !== 'production') {
      if (!selector) {
        throw new Error(`You must pass a selector to useSelector`)
      }
      if (typeof selector !== 'function') {
        throw new Error(`You must pass a function as a selector to useSelector`)
      }
      if (typeof equalityFn !== 'function') {
        throw new Error(
          `You must pass a function as an equality function to useSelector`
        )
      }
    }
    
    // 前面我们看源码的时候知道了,context会存储store和subscription
    const { store, subscription: contextSub } = useReduxContext()
	
    // 获得selectedState并返回
    const selectedState = useSelectorWithStoreAndSubscription(
      selector,
      equalityFn,
      store,
      contextSub
    )

    useDebugValue(selectedState)

    return selectedState
  }
}

来看看useSelectorWithStoreAndSubscription是如何返回我们想要的selectState:

function useSelectorWithStoreAndSubscription(
  selector,
  equalityFn,
  store,
  contextSub    // subscription
) {
  const [, forceRender] = useReducer((s) => s + 1, 0)
  
  // 创建一个新的订阅对象
  const subscription = useMemo(() => new Subscription(store, contextSub), [
    store,
    contextSub,
  ])
  
  const latestSubscriptionCallbackError = useRef()
  const latestSelector = useRef()
  const latestStoreState = useRef()
  const latestSelectedState = useRef()
  
  // 获得现在的store中的state
  const storeState = store.getState()
  let selectedState

  try {
    // 如果selector改变了、或者store中的state改变了、或者出现错误了
    // 那么就重新执行selector获得新的selectState
    // 否则直接返回上一次的selectState
    if (
      selector !== latestSelector.current ||
      storeState !== latestStoreState.current ||
      latestSubscriptionCallbackError.current
    ) {
      // 将new selectState 和 last selectState比较
      // 如果一致,返回上一次的selectState
      // 否则,返回new selectState
      const newSelectedState = selector(storeState)
      if (
        latestSelectedState.current === undefined ||
        !equalityFn(newSelectedState, latestSelectedState.current)
      ) {
        selectedState = newSelectedState
      } else {
        selectedState = latestSelectedState.current
      }
    } else {
      selectedState = latestSelectedState.current
    }
  } catch (err) {
    if (latestSubscriptionCallbackError.current) {
      err.message += `\nThe error may be correlated with this previous error:\n${latestSubscriptionCallbackError.current.stack}\n\n`
    }

    throw err
  }
  
  // 这个函数每次都被执行的时候,会保存这次的selector、store中的state、selectState和订阅捕获错误
  // 便于下次比较做使用
  useIsomorphicLayoutEffect(() => {
    latestSelector.current = selector
    latestStoreState.current = storeState
    latestSelectedState.current = selectedState
    latestSubscriptionCallbackError.current = undefined
  })
  
  // 当store或者subscription发生改变时
  // 会调用checkForUpdates获得新的storestate和selectState
  // 比较这次和上次的selectState的值
  // 如果不等,更新latestSelectedState和latestStoreState、latestSubscriptionCallbackError
  useIsomorphicLayoutEffect(() => {
    function checkForUpdates() {
      try {
        const newStoreState = store.getState()
        const newSelectedState = latestSelector.current(newStoreState)

        if (equalityFn(newSelectedState, latestSelectedState.current)) {
          return
        }

        latestSelectedState.current = newSelectedState
        latestStoreState.current = newStoreState
      } catch (err) {
        // we ignore all errors here, since when the component
        // is re-rendered, the selectors are called again, and
        // will throw again, if neither props nor store state
        // changed
        latestSubscriptionCallbackError.current = err
      }

      forceRender()
    }

    subscription.onStateChange = checkForUpdates
    subscription.trySubscribe()

    checkForUpdates()

    return () => subscription.tryUnsubscribe()
  }, [store, subscription])
  
  // 最后返回我们需要的selectedState
  return selectedState
}

useSelector内部主要通过useSelectorWithStoreAndSubscription来获得我们想要的selectStateuseSelectorWithStoreAndSubscription通过比较selectorstore判断是否要执行selector来获得最新的selectState,然后将新获取到了selectState与上一次selectState来做比较,判断返回哪一个值。内部为了方便比较,都会记录下当前的selector、store、selectState、和捕获的错误,目的就是、为了提高useSelector的性能,不用每次数据明明跟之前一样却要返回新的内容导致视图更新。

useDispatch

useDiapatch的本质就是获得store中的dispatch。举一个官方栗子:

import React, { useCallback } from 'react'
import { useDispatch } from 'react-redux'

export const CounterComponent = ({ value }) => {
const dispatch = useDispatch()
 const increaseCounter = useCallback(() => dispatch({ type: 'increase-counter' }), [])
  return (
   <div>
     <span>{value}</span>
      <button onClick={increaseCounter}>Increase counter</button>
   </div>
 )
}

直接看源码:

export function createDispatchHook(context = ReactReduxContext) {
  const useStore =
    context === ReactReduxContext ? useDefaultStore : createStoreHook(context)

  return function useDispatch() {
    const store = useStore()
    return store.dispatch
  }
}
export const useDispatch = /*#__PURE__*/ createDispatchHook()

不难理解,先获得Context,再获得Context中的store,最后返回store.dispatch

useStore

useDispatch中,有一个代码是createStoreHook(context),其实createStoreHook就是useStore的实现:

export function createStoreHook(context = ReactReduxContext) {
  const useReduxContext =
    context === ReactReduxContext
      ? useDefaultReduxContext
      : () => useContext(context)
  return function useStore() {
    const { store } = useReduxContext()
    return store
  }
}
export const useStore = /*#__PURE__*/ createStoreHook()

先获得Context,再获得Context中的store,最后将store返回~

小结

源码断断续续看了一周,还来回看了几遍,有点绕。官网虽然说在不久会废弃connectAdvanced,但是为了connect的实现,依旧得好好啃啊。通过学习这次源码也对useMemo这个钩子加深了印象,知道了mapStateToProps和mapDispacthToProps可以将ownProps当作第二个参数传入对性能的影响,最后就是学习到了内部新带hook,还接触到了reselect这个库。

最后,一定要多翻翻英文文档啊~

参考


如有错误,欢迎指出,感谢阅读~