关于javascript:源码分析reactredux

68次阅读

共计 40480 个字符,预计需要花费 102 分钟才能阅读完成。

前言

后面学习 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、NaN false true
0、+0 true true
0、-0 true false
+0、-0 true false
// 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 这个库。

最初,肯定要多翻翻英文文档啊~

参考

  • react-redux 官网
  • 庖丁解牛 React-Redux(一): connectAdvanced
  • Polyfill
  • 你真的理解浅比拟么?

如有谬误,欢送指出,感激浏览~

正文完
 0