引言
React 源码系列继续进行,今天来讲讲 Context 相关的内容。从何讲起呢?我们还是先从一个案例开始吧:
1 | import React, { Component } from 'react' |
上述代码使用了 React 已废弃的 Context API。Parent 组件提供了一个 context,该 context 只在孙组件 Grandson 里面用到了。既然这样,那 context 变化的时候子组件 Son 不应该调用 render 方法(目前是会的)。所以,我们用 PureComponent 来优化一下:
1 | class Son extends PureComponent { |
现在 Son 组件确实不会调用 render 方法了,然而悲剧的是 Grandson 也不会更新了。原因在于 Son 组件协调时进入了 bailout 逻辑,阻断了子组件的更新:
1 | function bailoutOnAlreadyFinishedWork( |
但是如果换成新的 API 则不会有这个问题:
1 | import React, {Component, PureComponent} from 'react' |
明显新 API 的效果更符合常理,那么它到底是怎么实现的呢?接下来就分析一下新 API 的源码,来解答一下我们的疑惑。
新的 Context API 源码分析
createContext
作为使用 Context 的第一步,首先当然要看看 createContext 做了什么:
1 |
|
可以看到, createContext 返回了一个 context 对象,该对象上有 _currentValue 属性(用来存储当前 context 的值)以及 Provider、 Consumer 属性(两种类型的组件)。接下来我们看看 Provider 是怎么处理更新的。
Provider
从下面的代码可知,Provider 的更新首先是取到最新的 value,并调用 pushProvider 将其更新到 context 的 _currentValue 之上: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
38function updateContextProvider(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
const providerType: ReactProviderType<any> = workInProgress.type;
const context: ReactContext<any> = providerType._context;
const newProps = workInProgress.pendingProps;
const oldProps = workInProgress.memoizedProps;
const newValue = newProps.value;
...
pushProvider(workInProgress, context, newValue);
...
}
export function pushProvider<T>(
providerFiber: Fiber,
context: ReactContext<T>,
nextValue: T,
): void {
if (isPrimaryRenderer) {
push(valueCursor, context._currentValue, providerFiber);
context._currentValue = nextValue;
...
} else {
push(valueCursor, context._currentValue2, providerFiber);
context._currentValue2 = nextValue;
...
}
}
剩下的语句我们先不分析,我们来看看 Consumer 更新时是怎么处理的。
Consumer
更新 Consumer 时会调用 updateContextConsumer: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
27function updateContextConsumer(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
let context: ReactContext<any> = workInProgress.type;
...
const newProps = workInProgress.pendingProps;
const render = newProps.children;
...
prepareToReadContext(workInProgress, renderLanes);
const newValue = readContext(context, newProps.unstable_observedBits);
let newChildren;
if (__DEV__) {
...
} else {
newChildren = render(newValue);
}
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, newChildren, renderLanes);
return workInProgress.child;
}
该函数中的核心方法是 readContext,即从 context 读取最新的值:
1 |
|
所谓读取 context 最新的值,也很简单,看当前渲染器是不是 primary,如果是就返回 context._currentValue,不是就返回 context._currentValue2。同时还会标记当前 Fiber 节点对于 context 的依赖,该依赖对于后续 context 的更新非常有用。从代码中可以看到有时 Fiber 节点可能会依赖多个 context,形成一条依赖链表,这种情况出现在函数组件中使用 useContext hooks 时。下图更加直观地表示了各 Fiber 的 dependencies:

答案揭晓
了解了 Provider 和 Consumer 后,接下来我们分析一下文章开始的问题:为什么 Context 更新可以透过经过了 bailout 的组件往下传递?答案还是在 updateContextProvider:
1 | function updateContextProvider( |
当 context 的新值相对旧值有变化时会执行 propagateContextChange,该函数的作用就是沿着 workInProgress 往下深度优先的遍历子树,找到依赖当前 context 的 Fiber,更新他们的 lanes(lanes 可以理解为更新的优先级),同时更新他们的祖先 Fiber 的 childLanes。比如说下面这个例子:

当进入某个 Fiber 的 bailout 时,如果检测到当前 Fiber 的 lanes 和 renderLanes 有交集时,会继续协调其子节点:
1 | function bailoutOnAlreadyFinishedWork( |
总结
本文通过一个案例引出了 React 中新旧 Context 处理组件更新的一些不同,并着重分析了新 API 的实现思路并解释了为什么 Context 更新可以透过经过了 bailout 的组件往下传递。