Fork me on GitHub

当我们connect组件时到底发生了什么

  使用redux进行状态控制的同学对connect肯定都不陌生了,我们都知道通过connect可以将store中的状态绑定到当前组件的props上,其中涉及到一些Context API的使用,但是究竟是什么触发了我们绑定组件的rerender呢,这个执行时机底层是怎么处理的呢,这成为了本文的研究主题。

  本文主要进行react-redux源码阅读,基于7.2.2版本。

组件

  可以看到react-redux在组件目录下仅有3个文件,Context.js最简单,就是导出通过Context API初始化一个Context对象并导出。我们主要看Provider.jsconnectAdvanced做了些什么。

Provider.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function Provider({ store, context, children }) {
const contextValue = useMemo(() => {
const subscription = new Subscription(store)
subscription.onStateChange = subscription.notifyNestedSubs
return {
store,
subscription,
}
}, [store])

const previousState = useMemo(() => store.getState(), [store])

useEffect(() => {
const { subscription } = contextValue
subscription.trySubscribe()

if (previousState !== store.getState()) {
subscription.notifyNestedSubs()
}
return () => {
subscription.tryUnsubscribe()
subscription.onStateChange = null
}
}, [contextValue, previousState])

const Context = context || ReactReduxContext

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

  可以看到当前版本下封装的Provider组件是基于Hooks构造的,主要做了几件事情:

  1. 通过useMemo检测store是否产生变化,返回一个记忆值对象,它由新的store以及对新store进行订阅的subscription实例组成,其中有一个将notifyNestedSubs属性赋值给onStateChange的动作,我们后文会分析为什么(subscriptionSubscription构造生成,后文会分析该构造函数,这里可以简单理解成是一个发布订阅模式的实现)。
  2. 通过useMemo检测store是否产生变化,记录变化前的storestate
  3. 取1,2的记忆值作deps,在useEffect中进行发布订阅的控制,此处能看到的逻辑是,初始化会对消息订阅器(subscription)进行初始订阅,之后当store的状态发生改变后,通知子孙订阅节点(notifyNestedSubs。后文会集中分析Subscription内部实现。

connectAdvanced.js

  这个文件代码内容就比较多了…494行,慢慢看其实也不复杂,先折叠代码看下整体结构:

  先从头部导入的内容来说,像那些工具类校验方法的我们就跳过了,这里简单聊一下hoist-non-react-staticsuseIsomorphicLayoutEffect

hoist-non-react-statics

  首先,这是一个进行静态方法拷贝的工具库。主要应用在我们进行HOC高阶组件封装的场景。因为当我们进行HOC设计时,被增强的内部函数上绑定的静态方法是无法被映射到增加后的函数上的,这需要我们手动拷贝。拿官方的例子来说:

1
2
3
4
5
6
7
// Define a static method
WrappedComponent.staticMethod = function() {/*...*/}
// Now apply a HOC
const EnhancedComponent = enhance(WrappedComponent);

// The enhanced component has no static method
typeof EnhancedComponent.staticMethod === 'undefined' // true

  我们自己要去重新绑定的话,就会出现类似下面这样的方式:

1
2
3
4
5
6
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
// Must know exactly which method(s) to copy :(
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}

  但是一个React组件上绑定的静态属性还是比较多的,不同类型间还需要区分。目前社区中其实已经有比较成熟的库做了这件事,就是hoist-non-react-statics,通过该库,我们可以比较容易的通过设置我们返回的高阶函数组件targetComponent以及需要拷贝的源函数组件sourceComponent进行静态内容拷贝(若是有不想拷贝的静态方法,也可以传第三个可选参数,进行过滤表设置)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// official usage
import hoistNonReactStatics from 'hoist-non-react-statics';

hoistNonReactStatics(targetComponent, sourceComponent);

hoistNonReactStatics(targetComponent, sourceComponent, { myStatic: true, myOtherStatic: true });

// react documents usage
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
hoistNonReactStatic(Enhance, WrappedComponent);
return Enhance;
}

useIsomorphicLayoutEffect

  其实这个方法就是一个使用useEffect还是useLayoutEffect的问题,两者的差别主要是执行时机上的,前者异步发生在Render阶段,后者同步发生在Commit阶段。而根据这个方法的isomorphic我们也知道这其实是一个兼容同构的api。从源码的判断和注释也可以分析出来,另外在react-native中和node一样也是直接使用useLayoutEffect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// useIsomorphicLayoutEffect.native.js
import { useLayoutEffect } from 'react'

// Under React Native, we know that we always want to use useLayoutEffect

export const useIsomorphicLayoutEffect = useLayoutEffect

// useIsomorphicLayoutEffect.js
import { useEffect, useLayoutEffect } from 'react'

// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser. We need useLayoutEffect to ensure the store
// subscription callback always has the selector from the latest render commit
// available, otherwise a store update may happen between render and the effect,
// which may cause missed updates; we also must ensure the store subscription
// is created synchronously, otherwise a store update may occur before the
// subscription is created and an inconsistent state may be observed

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

庖丁解牛

  了解了上述内容后,我们继续源码的阅读,先看折叠的第13行,发现其实是一个将组件字符串化的方法,目的是为了后面校验高阶函数入参是否符合标准React元素,如果不符合会抛出一个Error字符串,将我们传入的组件信息以字符串格式输出。了解后,发现没什么营养,继续往后面看第21行。

storeStateUpdatesReducer
1
2
3
4
function storeStateUpdatesReducer(state, action) {
const [, updateCount] = state
return [action.payload, updateCount + 1]
}

  storeStateUpdatesReducer方法实际上是后面React HooksuseReducer的第一个入参。

1
2
3
4
5
6
const EMPTY_ARRAY = []
const initStateUpdates = () => [null, 0]
const [
[previousStateUpdateResult],
forceComponentUpdateDispatch,
] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)

  结合前后文,我们可以发现这个useReducer真实目的就是为了获取一个dispatch执行句柄,便于后续store状态更新后,子组件的rerender

useIsomorphicLayoutEffectWithArgs

  下面的useIsomorphicLayoutEffectWithArgs也比较简单,它是将前文我们提到的useIsomorphicLayoutEffect进行了一个工厂化处理,支持传入这个hook需要的函数、函数需要的入参以及重新触发函数的依赖。

1
2
3
4
5
6
7
function useIsomorphicLayoutEffectWithArgs(
effectFunc,
effectArgs,
dependencies
) {
useIsomorphicLayoutEffect(() => effectFunc(...effectArgs), dependencies)
}
captureWrapperProps

  captureWrapperProps这个方法实际上是用在后面HOC函数wrapWithConnect中的一个hook回调,每当这个高阶组件重新渲染时,captureWrapperProps就会被重新执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function captureWrapperProps(
lastWrapperProps,
lastChildProps,
renderIsScheduled,
wrapperProps,
actualChildProps,
childPropsFromStoreUpdate,
notifyNestedSubs
) {
// We want to capture the wrapper props and child props we used for later comparisons
lastWrapperProps.current = wrapperProps
lastChildProps.current = actualChildProps
renderIsScheduled.current = false

// If the render was from a store update, clear out that reference and cascade the subscriber update
if (childPropsFromStoreUpdate.current) {
childPropsFromStoreUpdate.current = null
notifyNestedSubs()
}
}

  从captureWrapperProps内部的使用来说,我们可以很容易发现lastWrapperPropslastChildPropsrenderIsScheduledchildPropsFromStoreUpdate都是通过useRef进行黑盒状态保存的。其中的值不会被rerender所改变。代码块干的事情主要为:

  1. 在每次render中更新最新的我们导出HOC的入参props,实际上它是解构后的属性,第一个入参是操作当前节点的ref
1
2
3
4
5
6
7
8
9
10
11
const [
propsContext,
reactReduxForwardedRef,
wrapperProps,
] = useMemo(() => {
// Distinguish between actual "data" props that were passed to the wrapper component,
// and values needed to control behavior (forwarded refs, alternate context instances).
// To maintain the wrapperProps object reference, memoize this destructuring.
const { reactReduxForwardedRef, ...wrapperProps } = props
return [props.context, reactReduxForwardedRef, wrapperProps]
}, [props])
  1. 在每次render中更新最新的返回函数的props
  2. 标记正在进行render
  3. 如果发现是由store更新引起的rerender,通知所有子订阅节点进行更新
subscribeUpdates

  subscribeUpdates同样在wrapWithConnect的一个hook回调中,不过它只有在它的deps[store, subscription, childPropsSelector]改变后才重新触发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
function subscribeUpdates(
shouldHandleStateChanges,
store,
subscription,
childPropsSelector,
lastWrapperProps,
lastChildProps,
renderIsScheduled,
childPropsFromStoreUpdate,
notifyNestedSubs,
forceComponentUpdateDispatch
) {
// If we're not subscribed to the store, nothing to do here
if (!shouldHandleStateChanges) return

// Capture values for checking if and when this component unmounts
let didUnsubscribe = false
let lastThrownError = null

// We'll run this callback every time a store subscription update propagates to this component
const checkForUpdates = () => {
if (didUnsubscribe) {
// Don't run stale listeners.
// Redux doesn't guarantee unsubscriptions happen until next dispatch.
return
}

const latestStoreState = store.getState()

let newChildProps, error
try {
// Actually run the selector with the most recent store state and wrapper props
// to determine what the child props should be
newChildProps = childPropsSelector(
latestStoreState,
lastWrapperProps.current
)
} catch (e) {
error = e
lastThrownError = e
}

if (!error) {
lastThrownError = null
}

// If the child props haven't changed, nothing to do here - cascade the subscription update
if (newChildProps === lastChildProps.current) {
if (!renderIsScheduled.current) {
notifyNestedSubs()
}
} else {
// Save references to the new child props. Note that we track the "child props from store update"
// as a ref instead of a useState/useReducer because we need a way to determine if that value has
// been processed. If this went into useState/useReducer, we couldn't clear out the value without
// forcing another re-render, which we don't want.
lastChildProps.current = newChildProps
childPropsFromStoreUpdate.current = newChildProps
renderIsScheduled.current = true

// If the child props _did_ change (or we caught an error), this wrapper component needs to re-render
forceComponentUpdateDispatch({
type: 'STORE_UPDATED',
payload: {
error,
},
})
}
}

// Actually subscribe to the nearest connected ancestor (or store)
subscription.onStateChange = checkForUpdates
subscription.trySubscribe()

// Pull data from the store after first render in case the store has
// changed since we began.
checkForUpdates()

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

if (lastThrownError) {
// It's possible that we caught an error due to a bad mapState function, but the
// parent re-rendered without this component and we're about to unmount.
// This shouldn't happen as long as we do top-down subscriptions correctly, but
// if we ever do those wrong, this throw will surface the error in our tests.
// In that case, throw the error from here so it doesn't get lost.
throw lastThrownError
}
}

return unsubscribeWrapper
}

  内容比较多,我们一点点看:

  1. 首先有个标记变量shouldHandleStateChanges用于判断我们是否需要处理该回调,而该参数我们没配置connectOptions时,默认情况为true
  2. 通过didUnsubscribelastThrownError进行组件是否卸载以及是否抛出异常的记录。
  3. checkForUpdates是挂载给消息订阅实例subscriptiononStateChange方法上的。在该方法内:
  • 首先判断了当前组件是否已卸载,卸载了就直接return否则会继续执行,否则根据最近store状态进行selector,得到最新的孩子props
  • 如果最新的孩子属性和之前的相同,则进行级联的子节点订阅器消息通讯(本质上是一个链表结构,后文会看),否则会更新组件内部的ref快照属性lastChildPropschildPropsFromStoreUpdaterenderIsScheduled
  • 此时确认属性确实发生了更新,会调用前文我们说的dispatch句柄,进行当前组件的rerender
  • 至于为什么会rerender,因为这个useReducer返回的更新内容作为了actualChildPropsdeps,而actualChildProps又作为了我们最终返回渲染元素的deps
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const actualChildProps = usePureOnlyMemo(() => {
// Tricky logic here:
// - This render may have been triggered by a Redux store update that produced new child props
// - However, we may have gotten new wrapper props after that
// If we have new child props, and the same wrapper props, we know we should use the new child props as-is.
// But, if we have new wrapper props, those might change the child props, so we have to recalculate things.
// So, we'll use the child props from store update only if the wrapper props are the same as last time.
if (
childPropsFromStoreUpdate.current &&
wrapperProps === lastWrapperProps.current
) {
return childPropsFromStoreUpdate.current
}

// TODO We're reading the store directly in render() here. Bad idea?
// This will likely cause Bad Things (TM) to happen in Concurrent Mode.
// Note that we do this because on renders _not_ caused by store updates, we need the latest store state
// to determine what the child props should be.
return childPropsSelector(store.getState(), wrapperProps)
}, [store, previousStateUpdateResult, wrapperProps])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Now that all that's done, we can finally try to actually render the child component.
// We memoize the elements for the rendered child component as an optimization.
const renderedWrappedComponent = useMemo(
() => (
<WrappedComponent
{...actualChildProps}
ref={reactReduxForwardedRef}
/>
),
[reactReduxForwardedRef, WrappedComponent, actualChildProps]
)

// If React sees the exact same element reference as last time, it bails out of re-rendering
// that child, same as if it was wrapped in React.memo() or returned false from shouldComponentUpdate.
const renderedChild = useMemo(() => {
if (shouldHandleStateChanges) {
// If this component is subscribed to store updates, we need to pass its own
// subscription instance down to our descendants. That means rendering the same
// Context instance, and putting a different value into the context.
return (
<ContextToUse.Provider value={overriddenContextValue}>
{renderedWrappedComponent}
</ContextToUse.Provider>
)
}

return renderedWrappedComponent
}, [ContextToUse, renderedWrappedComponent, overriddenContextValue])

return renderedChild
connectAdvanced

  终于回到正主了…首先connectAdvanced接收两个入参selectorFactoryconnectOptions

selectorFactory

  selectorFactory实际上内容存在于src/connect/selectorFactory.js中,此处我们简单说下是做什么的,其实react-redux作者也在源码中提供了注释:返回了一个提供了从新statepropsdispatch变化中计算新props能力的函数

connectOptions

  connectOptions主要是一些对于该connect生成函数的配置项:

  • 计算生成获取HOC函数名的getDisplayName方法。
  • HOC的方法名methodName,默认值为connectAdvanced
  • renderCountProp,结合react devtools分析是否有冗余的render,默认值为undefined
  • shouldHandleStateChanges,决定当前HOC是否订阅store变化,默认值为true
  • storeKeypropscontext通过该key值可以访问store,默认值为store
  • withRef,旧版本的react通过refs访问,默认值为false
  • forwardRef,通过reactforwardRefAPI暴露ref,默认值为false
  • context,Context API消费者使用的context,默认值为前文我们生成的ReactReduxContext
ConnectFunction

  代码主体内容,其实可以分为三部分:

  1. 传参校验
  2. 主体ConnectFunction方法实现
  3. HOC恢复处理(displayName及静态属性复制等)

  1,3其实没什么好说的,我们主要看ConnectFunction做了什么:

  1. 提取传入的context属性,因为除了react-redux本身自定义初始化的属性外,用户自己也可能会传一个自定义的。获取ref,及其余props
  2. 根据真实获取到的context进行后续判断使用。
  3. 判断store属性是否存在,会从propscontext中尝试读取,若没有则抛出异常。
  4. childPropsSelector每次会在store更新后重新生成一个selector方法供相关取其作deps的钩子进行新props映射。
  5. subscription订阅构成链式

  5中的subscription订阅器构造链表结构我认为是connect步骤中最核心的一步,没有它我们就无法完成整个状态树的更新。先看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    const [subscription, notifyNestedSubs] = useMemo(() => {
// const NO_SUBSCRIPTION_ARRAY = [null, null]
if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY

// This Subscription's source should match where store came from: props vs. context. A component
// connected to the store via props shouldn't use subscription from context, or vice versa.
const subscription = new Subscription(
store,
didStoreComeFromProps ? null : contextValue.subscription
)

// `notifyNestedSubs` is duplicated to handle the case where the component is unmounted in
// the middle of the notification loop, where `subscription` will then be null. This can
// probably be avoided if Subscription's listeners logic is changed to not call listeners
// that have been unsubscribed in the middle of the notification loop.
const notifyNestedSubs = subscription.notifyNestedSubs.bind(
subscription
)

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

  shouldHandleStateChanges这个控制是否订阅的标记,实际上就是对齐了我们connect组件时,如果仅是为了使用其dispatch的能力,而不需要订阅store中的值进行rerender的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
return connectHOC(selectorFactory, {
// used in error messages
methodName: 'connect',

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

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

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

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

  核心就是看你有没有配置mapStateToProps,所以我们平时使用要注意场景,无需订阅的,传null就行。

1
2
3
export default connect(null, {
// 绑定dispatch
})(App)

  之后就是根据store, didStoreComeFromProps, contextValue这些deps进行subscription实例的重新构造,其中有一个判断主要是区分store是从props数据源来的还是context中获取的。然后提取实例方法notifyNestedSubs绑定当前实例上下文,返回tuple供后续的钩子使用。这里的钩子就是指前文中我们聊过的进行subscribeUpdates回调的钩子,在里面会重新发起trySubscribe订阅。

  要了解subscription是怎么产生关联的我们就要分析Subscription.js文件了。

Subscription.js

  Subscription.js主要由两部分组成,一个是链表结构生成器,里面提供了基本的清空、通知调用、获取所有订阅内容以及订阅和取消订阅的方法。另外一部分就是暴露出去的Subscription类,用于前者的调度操作。

createListenerCollection

  该函数中实际上只有notifysubscribe方法值得我们留意一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const batch = getBatch()
let first = null
let last = null

notify() {
batch(() => {
let listener = first
while (listener) {
listener.callback()
listener = listener.next
}
})
},
subscribe(callback) {
let isSubscribed = true

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

if (listener.prev) {
listener.prev.next = listener
} else {
first = listener
}

return function unsubscribe() {
if (!isSubscribed || first === null) return
isSubscribed = false

if (listener.next) {
listener.next.prev = listener.prev
} else {
last = listener.prev
}
if (listener.prev) {
listener.prev.next = listener.next
} else {
first = listener.next
}
}
},

  notify其实很简单,就是从头指针,遍历整个链表,执行所有回调。subscribe则进行一个listener实体的构造,由prevnextcallback组成,是我们消息订阅节点的基本结构。另外它最终会返回一个取消当前节点订阅的句柄方法,供我们使用。细节结合Subscription看会更清晰。

Subscription

  Subscription要理解其实要分初始化,及和后代子节点建立关联的情景来看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
export default class Subscription {
constructor(store, parentSub) {
this.store = store
this.parentSub = parentSub
this.unsubscribe = null
this.listeners = nullListeners

this.handleChangeWrapper = this.handleChangeWrapper.bind(this)
}

addNestedSub(listener) {
this.trySubscribe()
return this.listeners.subscribe(listener)
}

notifyNestedSubs() {
this.listeners.notify()
}

handleChangeWrapper() {
if (this.onStateChange) {
this.onStateChange()
}
}

isSubscribed() {
return Boolean(this.unsubscribe)
}

trySubscribe() {
if (!this.unsubscribe) {
this.unsubscribe = this.parentSub
? this.parentSub.addNestedSub(this.handleChangeWrapper)
: this.store.subscribe(this.handleChangeWrapper)

this.listeners = createListenerCollection()
}
}

tryUnsubscribe() {
if (this.unsubscribe) {
this.unsubscribe()
this.unsubscribe = null
this.listeners.clear()
this.listeners = nullListeners
}
}
}

  构造函数中主要是传入最新的store内容及上一个订阅器节点parentSub。核心代码主要是trySubscribe方法,它做了这么几件事:

  1. 当该subscription实例未订阅时(通常都是初始化情况,也有可能被手动取消了订阅),判断是否有parentSub传入,即确认下是否为根订阅器。
  2. 如果是根订阅器,则我们直接使用react-reduxstore自带的subscribe方法进行订阅,这个方法会在store内容改变时进行回调。同样这个store自带的订阅方式也会返回一个移除监听的句柄。
  3. 如果非根订阅器,就会走我们的createListenerCollection构造的链表节点的subscribe订阅方法,同样也会拿到取消订阅的句柄。这里要注意的是,如果存在父订阅器传入,实际上是在父订阅器上添加callback

总结

  看到这里,实际上connect要了解的代码也看得差不多了,我的疑惑是,我们不同组件的connect是如何关联到一起的呢?下面细品一下:

  1. HOC中通过useMemo获取一个[subscription, notifyNestedSubs]的tuple。每次deps更新,其中的实例和方法会重新生成和绑定。
  2. 同样通过useMemo计算一个能够重载更新的context值,功能主要是为了将1中生成的subscription作为context值关联进去:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Determine what {store, subscription} value should be put into nested context, if necessary,
// and memoize that value to avoid unnecessary context updates.
const overriddenContextValue = useMemo(() => {
if (didStoreComeFromProps) {
// This component is directly subscribed to a store from props.
// We don't want descendants reading from this store - pass down whatever
// the existing context value is from the nearest connected ancestor.
return contextValue
}

// Otherwise, put this component's subscription instance into context, so that
// connected descendants won't update until after this component is done
return {
...contextValue,
subscription,
}
}, [didStoreComeFromProps, contextValue, subscription])
  1. 这个新的contextoverriddenContextValue会被我们最终connect的组件所访问:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// If React sees the exact same element reference as last time, it bails out of re-rendering
// that child, same as if it was wrapped in React.memo() or returned false from shouldComponentUpdate.
const renderedChild = useMemo(() => {
if (shouldHandleStateChanges) {
// If this component is subscribed to store updates, we need to pass its own
// subscription instance down to our descendants. That means rendering the same
// Context instance, and putting a different value into the context.
return (
<ContextToUse.Provider value={overriddenContextValue}>
{renderedWrappedComponent}
</ContextToUse.Provider>
)
}

return renderedWrappedComponent
}, [ContextToUse, renderedWrappedComponent, overriddenContextValue])

return renderedChild
  1. contextValue通过源码分析,我们可以得到它本质上是生成后的HOC传下来的属性props.context。这里有同学就表示很迷惑了,我们不是只是connect一个组件嘛,哪里传了这玩意?我们先按下不表,往后看。

  2. 前文中我们知道在实体方法ConnectFunction中还进行了subscription的更新。subscribeUpdates回调中进行了订阅器subscription上的listeners链表节点的callback更新。这个callback做了什么呢?如果传入connect组件的属性没有发生改变,会使用1中的notifyNestedSubs,将链表上的callback依次执行。否则就会更新属性并进行当前组件的rerender。另外它除了绑定在当前subscriptionlisteners上外,钩子触发时,也会拉取一次,目的是在首次render后拉取一次,以防这之间store发生了改变。

  3. 在阅读源码的过程中我发现了一个干扰因素,实际上就是context的入参问题,我们使用react-redux时,显少会直接将context作为props往下传,所以内部的context均视作ReactReduxContext会使逻辑更为清晰。

  4. 事实上,作为项目入口顶层的Provider已经将store和根subscription配置到了context内,即我们组件内的useContext可以直接读取,加以操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 通常项目入口
import React from 'react';
import { Provider } from 'react-redux';
import store from './store';
import BasicLayout from '@/layouts/BasicLayout';
import { BrowserRouter as Router } from 'react-router-dom';

let App = () => {
return (
<Provider store={store}>
<Router>
<BasicLayout />
</Router>
</Provider>
)
}

export default App;

// react-redux中的Provider

function Provider({ store, context, children }) {
const contextValue = useMemo(() => {
const subscription = new Subscription(store)
subscription.onStateChange = subscription.notifyNestedSubs
return {
store,
subscription,
}
}, [store])

const previousState = useMemo(() => store.getState(), [store])

useIsomorphicLayoutEffect(() => {
const { subscription } = contextValue
subscription.trySubscribe()

if (previousState !== store.getState()) {
subscription.notifyNestedSubs()
}
return () => {
subscription.tryUnsubscribe()
subscription.onStateChange = null
}
}, [contextValue, previousState])

const Context = context || ReactReduxContext

return <Context.Provider value={contextValue}>{children}</Context.Provider>
}
  1. 综上,我们发现实际上进行connect的组件,内部是一个独立的subscription。在我们常规的使用场景下,store这个值来源于顶层的ReactReduxContext.Provider。于是我们在connect的组件内部能读取到根的subscription,并将其作为Subscription构造函数的第二个入参,即parentSub,传入当前connect组件自身构造的subscription实例当中。并且会调用trySubscribe,将当前组件的checkForUpdates绑定到根subscription(或者说父级的)实例的listener链表上,这样当上层发生变化时,通过notify方法,就会遍历整个链表触发所有有子孙关系的组件进行rerender(通过子组件自己的forceComponentUpdateDispatch)。具体细节,前文也有了分析,比如当前组件会将自身的subscription覆盖顶层在contextValue中传下来的subscription,并将重载后组装的contextValue通过当前HOC中的Provider传下去给我们connect的组件。