在 React 中,Context 是一种用于在组件之间共享数据的方法,而无需通过逐层传递 props 来传递数据。它可以被看作是 React 组件树中的一个全局状态容器,使得某些数据能够被跨越组件层级传递,从而简化了组件之间数据传递的复杂性。
Context 通常由两个主要部分组成:Provider 和 Consumer:
-
Provider:Provider 组件负责将数据提供给子组件。它包裹在需要访问这些共享数据的组件外部,并通过 value 属性将数据传递给子组件。
-
Consumer:Consumer 组件允许子组件订阅 Context 中的数据。通过在组件内部渲染一个函数,您可以访问 Provider 提供的数据。
了解了关于 Context 的基本信息之后,废话少说,直接上号。
创建 Context
创建一个 Context,需要通过 React.createContext 这个 api 来创建 context 对象,在 React 源码中有如下代码所示:
在这个函数中首先创建一个 context 对象,并将初始值传递给 _currentValue
和 _currentValue2
,保存两个值是为了支持多个渲染器并发渲染,例如 React Native 和 React DOM 可能在同一个应用中并行工作。
_threadCount
:用于跟踪在单个渲染器内当前支持多少个并发渲染器,例如用于并行服务器渲染。
例如,在项目中我们创建了如下 Context:
jsx
import { createContext } from "react";
const Context = createContext("岂曰无衣");
export default Context;
这时候,我们通过 debugger 可以查看出有如下结构所示:
Provider 提供者
Context 中的 Provider 本质上是一个 React 组件,它的作用是提供一个特定的数据源,以便被子组件订阅并访问。Provider 组件在 React 的组件树中包裹着那些需要访问共享数据的子组件。
所以也就有以下的对比关系:
rust
jsx -> element -> fiber
<Provider> -> REACT_PROVIDER_TYPE React element -> ContextProvider fiber
在 Fiber 树渲染阶段,也就是调和阶段,会调用 beginWork 函数:
ts
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
// 清空过期时间
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
case ContextProvider:
return updateContextProvider(current, workInProgress, renderLanes);
case ContextConsumer:
return updateContextConsumer(current, workInProgress, renderLanes);
}
}
如果 workInProgress.tag 是 ContextProvider 类型的话会调用 updateContextProvider 函数进行相应的处理。
首先我们来看看此时 fiber 树的结构:
workInProgress 是正在进行中的工作单元,表示正在进行更新的组件实例。而 workInProgress.pendingProps 则是这个正在进行的工作单元所持有的最新的 props,也就是即将应用到组件上的 props。
这也证实了我们前面中所说到的 Provider 本质上是一个 React 组件。而它的 children 也就是被 Provider 包裹的组件:
jsx
const App = () => {
return (
<Context.Provider value={"与子同袍"}>
<Moment />
</Context.Provider>
);
};
这就完美对应上了。
在 beginWork 函数中,如果当前类型的 fiber 不需要更新,那么会 FinishedWork 中止当前节点和子节点的更新。
如果当前类型 fiber 需要更新,那么会调用不同类型 fiber 的处理方法。 updateContextProvider 方法就是用来更新 Context 的。
updateContextProvider
该函数的主要功能如下代码所示:
ts
function updateContextProvider(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
) {
const newProps = workInProgress.pendingProps;
const oldProps = workInProgress.memoizedProps;
const newValue = newProps.value;
pushProvider(workInProgress, context, newValue);
if (enableLazyContextPropagation) {
} else {
if (oldProps !== null) {
const oldValue = oldProps.value;
if (is(oldValue, newValue)) {
// No change. Bailout early if children are the same.
if (
oldProps.children === newProps.children &&
!hasLegacyContextChanged()
) {
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes
);
}
} else {
propagateContextChange(workInProgress, context, renderLanes);
}
}
}
const newChildren = newProps.children;
reconcileChildren(current, workInProgress, newChildren, renderLanes);
return workInProgress.child;
}
在该函数中首先会调用 pushProvider 函数将最新的值赋值给 context._currentValue
和 context._currentValue2
。
接下来检查旧的上下文值是否与新值相同。如果相同,则检查子树是否相同,如果相同,则直接跳过这个子树的更新并使用 bailoutOnAlreadyFinishedWork 进行优化。
如果上下文值不同,表示上下文发生了变化,会调用 propagateContextChange 函数来通知所有依赖此 context 的 fiber,并标记相关 Consumer 需要进行更新。
通过 reconcileChildren 函数,对 Provider 的子元素进行调和,处理子元素的更新。
返回 workInProgress.child,即 Provider 的第一个子元素。
在浏览器终端有这样的对象结构,如下图所示:
其中 "岂曰无衣"
是刚开始创建时传递的初始值,而 "与子同袍"
是我们在调用 Provider 传递的值。
propagateContextChange
接下里我们详细讲讲这个函数,它为子组件中,所有依赖此 Context 的组件安排一个更新,接下来进入到该函数中,由于核心逻辑在 propagateContextChange_eager 中,直接看 propagateContextChange_eager 代码吧:
ts
function propagateContextChange_eager<T>(
workInProgress: Fiber,
context: ReactContext<T>,
renderLanes: Lanes
): void {
let fiber = workInProgress.child;
if (fiber !== null) {
fiber.return = workInProgress;
}
// 从子节点开始匹配是否存在消费了当前 Context 的节点
while (fiber !== null) {
let nextFiber;
const list = fiber.dependencies;
if (list !== null) {
nextFiber = fiber.child;
let dependency = list.firstContext;
while (dependency !== null) {
// 1. 判断 fiber 节点的 context 和当前 context 是否匹配
if (dependency.context === context) {
// 2. 匹配时,给当前节点调度一个更新任务
if (fiber.tag === ClassComponent) {
}
fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
}
// 3. 向上标记 childLanes
scheduleContextWorkOnParentPath(
fiber.return,
renderLanes,
workInProgress
);
list.lanes = mergeLanes(list.lanes, renderLanes);
break;
}
dependency = dependency.next;
}
} else if (fiber.tag === ContextProvider) {
} else if (fiber.tag === DehydratedFragment) {
} else {
}
fiber = nextFiber;
}
}
该函数的主要功能有以下两个方面:
- 从 ContextProvider 的节点出发,向下查找所有 fiber.dependencies 依赖当前 Context 的节点。
- 找到消费节点时,从当前节点出发,向上回溯标记父节点 fiber.childLanes,标识其子节点需要更新,从而保证了所有消费了该 Context 的子节点都会被重新渲染,实现了 Context 的更新
Context 会强制安排更新,无视 PureComponent、Memo、shouldComponentUpdate 等优化逻辑。
如上面图中代码所示,当我们 Context 发生了变化,就算 memo 包裹的组件也会重新渲染。
当子组件的上层多次使用了同一个 Context 的 Provider 时,子组件会获取离自己最近的那个 Provider 所提供的上下文值。也就是说,React 会沿着组件树自上而下的顺序,查找最近的 Provider,并使用它提供的值。
到这里的时候,Provider 已经基本讲解结束,但是 Consumer 已经基本不适用了,现在都是在用 useContext 了。
useContext
在前面的例子中我们已经知道了 useContext 需要将 createContext 创建的 Context 作为参数进行调用。
mount 阶段
useContext 在 mount 时主要会调用 readContext 函数:
那么我们来看看 readContext 函数是在如何工作的,如下代码所示:
ts
export function readContext<T>(context: ReactContext<T>): T {
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
if (lastFullyObservedContext === context) {
} else {
const contextItem = {
context: ((context: any): ReactContext<mixed>),
memoizedValue: value,
next: null,
};
if (lastContextDependency === null) {
lastContextDependency = contextItem;
currentlyRenderingFiber.dependencies = {
lanes: NoLanes,
firstContext: contextItem,
};
if (enableLazyContextPropagation) {
currentlyRenderingFiber.flags |= NeedsPropagation;
}
} else {
// Append a new context item.
lastContextDependency = lastContextDependency.next = contextItem;
}
}
return value;
}
在该函数中,主要有以下几个核心逻辑:
-
创建一个 contextItem 对象。 该对象的数据如下图所示:
其中 next 是指向下一个 context 项
-
fiber 上会存在多个 dependencies,它们以链表的形式联系到一起,如果不存在最后一个 context dependency,context dependencies 为空 ,那么会创建第一个 dependency。
-
如果存在最后一个 dependency,那么 contextItem 会以链表形式保存,并变成最后一个 lastContextDependency。 这就是我们前面中说到的 React 会沿着组件树自上而下的顺序,查找最近的 Provider,并使用它提供的值。
-
最终返回 value,也就是 Provider 提供的值:
返回该值之后继续执行 Fiber 树构造直到全部完成之后。
没了,挺简单的。
原理总结
通过 React.createContext() 创建一个 Context 对象。这个对象包含 Provider 和 Consumer 组件以及一些其他属性。
使用 <Context.Provider>
组件将需要共享的数据包裹在内,通过 value 属性传递数据。
使用 <Context.Consumer>
组件或 useContext Hook 在需要的组件中访问共享数据。这两种方式都会在组件树中向上查找最近的匹配的 Provider,也就是 Fiber 树上 dependencies,并提取其 value 属性传递给组件。
当 Provider 提供的数据发生变化时,会触发依赖于这些数据的组件的重新渲染,无论你是使用 PureComponent、Memo、shouldComponentUpdate 等优化逻辑。
我是如何爱上了 Context 的
我最近在使用 webContainer 开发一个在线代码编辑器,它就类似一个虚拟机,要在页面初始化时对其开机并保存其实例,后面都要用到,一开始我用的是 redux,接下来我们来看看 Redux 会给我们带来什么样的效果:
直接报错,原因是 redux 在存储之前会对数据进行序列化,将 JavaScript 对象转换成字符串的过程,以便在不同的环境中传输和存储数据,也就是说实例的方法都没了,那还玩个毛线。
于是我便使用了 Context 的方式去掉了 redux:
并将 webContainerInstance 通过 Provider 传递给其他组件使用:
从而可以使多个组件同时使用一个实例。
总结
巧用 Context,能解决你在项目中百分之 90 的场景。