Appearance
React.Context 源码详细解释
1. 概述
React.Context 是 React 提供的一种在组件树中共享数据的机制,无需显式地通过每一层组件传递 props。它主要用于解决多层组件间的数据传递问题,例如主题、用户登录状态等全局数据。
2. 使用
2.1. 旧版本上下文能力
在 v16.3.0 之前,React 使用的是 Class.contextTypes , Class.childContextTypes, Class.prototype.getChildContext 三个属性来实现上下文的能力,并借助于 PropTypes 来进行类型约束。具体如下:
2.1.1. 提供者
React 需要借助于 proptypes 包来申明 context 的类型,并进行类型约束。
Class.childContextTypes用于声明子组件可以接受的 context 类型。Class.prototype.getChildContext用于返回当前组件的 context 数据。
js
import PropTypes from 'proptypes'
Class MyContextProvider extends React.Component {
getChildContext() {
return {
key1: '张三',
};
}
render() {
return this.props.children;
}
}
MyContextProvider.childContextTypes = {
key1: PropTypes.string,
}2.1.2. 消费者
Class.contextTypes用于声明当前组件可以接受的 context 类型。Class.prototype.context用于获取当前组件的 context 数据。
js
import PropTypes from 'proptypes'
Class MyContextConsumer extends React.Component {
render() {
return <div>{this.context.key1}</div>;
}
}
MyContextConsumer.contextTypes = {
key1: PropTypes.string,
}2.1.3. 解答
在 React16.3.0 之后,React 官方推荐使用 createContext 来创建上下文对象,然后通过 Provider 组件提供数据,Consumer 组件消费数据。 但是对于旧版的 Context 能力可以自己去开启,所以源码里面依然看到这三个属性, 并根据 disableLegacyContext 这一个编译属性决定是否禁用旧版本上下文的能力。
2.1.4. 提供者
js
// 1. 在 updateContainer 函数中,获取当前组件的上下文对象
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function
): Lane {
const context = getContextForSubtree(parentComponent);
}
/**
* 主要是获取当前组件的上下文 Context
* @param {*} parentComponent
* @returns
*/
function getContextForSubtree(
parentComponent: ?React$Component<any, any>
): Object {
if (!parentComponent) {
return emptyContextObject;
}
// 获取父组件的 fiber 实例对象
const fiber = getInstance(parentComponent);
// 获取父组件的上下文对象
const parentContext = findCurrentUnmaskedContext(fiber);
if (fiber.tag === ClassComponent) {
const Component = fiber.type;
if (isLegacyContextProvider(Component)) {
return processChildContext(fiber, Component, parentContext);
}
}
return parentContext;
}在 updateContainer 函数中,会调用 getContextForSubtree 函数来处理旧版本上下文能力中提供者数据的处理。 其一方面通过向上遍历最近的 类型是 ClassComponent 且 是一个旧版本提供者类型的组件,获取其 node.stateNode.__reactInternalMemoizedMergedChildContext 的值作为父级组件的上下文对象。
一方面通过 processChildContext(fiber, Component, parentContext)函数,通过 Component.prototype.getChildContext() 来获取当前组件的上下文对象的值,并对返回的上下文对象的数据按照 childContextTypes 进行过滤,最后根据上面父组件的 parentContext 来合并返回。
具体如下
js
/**
* 处理组件的旧版上下文 context 提供者的逻辑, childContextTypes、getChildContext
* 1. 获取 context 提供者的上下文对象
* 2. 对 返回的上下文对象的数据按照 childContextTypes 进行过滤
* 3. 合并 当前上下文context 和 父级上下文 parentContext
* @param {*} fiber
* @param {*} type
* @param {*} parentContext
* @returns
*/
function processChildContext(
fiber: Fiber,
type: any,
parentContext: Object
): Object {
const instance = fiber.stateNode;
const childContextTypes = type.childContextTypes;
// 1. 获取 context 提供者的上下文对象
const childContext = instance.getChildContext();
// 2. 对 返回的上下文对象的数据按照 childContextTypes 进行过滤
for (const contextKey in childContext) {
if (!(contextKey in childContextTypes)) {
throw new Error(
`${
getComponentNameFromFiber(fiber) || "Unknown"
}.getChildContext(): key "${contextKey}" is not defined in childContextTypes.`
);
}
}
// 3. 合并 当前上下文context 和 父级上下文 parentContext
return { ...parentContext, ...childContext };
}2.1.5. 消费者
对于消费者,主要在处理组件的时候会使用旧版本 context 的能力,这时候就需要区分 ClassComponent 和 FunctionComponent 了。但是其实现逻辑是一样的,所以我们可以只看一下 ClassComponent 的处理逻辑。
js
// 1. 获取初始化的时候传入的 context 值
if (typeof contextType === "object" && contextType !== null) {
// 读取 Class.contextType 传入的 context 对象
context = readContext((contextType: any));
} else if (!disableLegacyContext) {
// 获取未处理的 context 上下文的值
unmaskedContext = getUnmaskedContext(workInProgress, ctor, true);
// 是否定义了 contextTypes 属性
const contextTypes = ctor.contextTypes;
isLegacyContextConsumer = contextTypes !== null && contextTypes !== undefined;
// 如果定义了 contextTypes 属性, 则将 unmaskedContext 转换 按照 contextTypes 进行过滤
// 否则直接返回空对象
context = isLegacyContextConsumer
? getMaskedContext(workInProgress, unmaskedContext)
: emptyContextObject;
}这边优先判断 contextType 是否是对象,从而优先使用 新版本的上下文能力。如果不支持新版本 且 没有禁用旧版本的上下文能力,才会使用旧版本的上下文能力。
- 获取上下文中所有的 context 的数据
js
/**
* 获取未过滤的 context 的值
* 获取的是 contextStackCursor.current 的值
* @param {*} workInProgress
* @param {*} Component
* @param {*} didPushOwnContextIfProvider
* @returns
*/
function getUnmaskedContext(
workInProgress: Fiber,
Component: Function,
didPushOwnContextIfProvider: boolean
): Object {
// 禁用旧版 contextTypes 则返回空对象
if (disableLegacyContext) {
return emptyContextObject;
} else {
if (didPushOwnContextIfProvider && isContextProvider(Component)) {
return previousContext;
}
// 返回 contextStackCursor.current 的值
return contextStackCursor.current;
}
}这边直接返回的是 contextStackCursor.current 的值, 这个就涉及到 旧版本上下文数据的存储方式了。具体看下面
- 根据 context 的数据通过 contextTypes 进行过滤,返回最终的 context 对象。
在获取到未过滤的 context 的值之后,会根据 contextTypes 进行过滤,返回最终的 context 对象。
js
/**
* 获取按照 contextTypes 过滤后的 context 对象
* 如果没有定义 contextTypes 则返回空对象
* 如果定义了 contextTypes 则按照 key 过滤 unmaskedContext 中的值
* 并返回过滤后的 context 对象
* 同时缓存到 react 的 instance.__reactInternalMemoizedUnmaskedChildContext 实例上
* @param {*} workInProgress
* @param {*} unmaskedContext
* @returns
*/
function getMaskedContext(
workInProgress: Fiber,
unmaskedContext: Object
): Object {
if (disableLegacyContext) {
return emptyContextObject;
} else {
const type = workInProgress.type;
// 获取组件上定义的 contextTypes 主要是 React16之前的方式
const contextTypes = type.contextTypes;
// 如果没有定义 contextTypes 则直接 { }
if (!contextTypes) {
return emptyContextObject;
}
const instance = workInProgress.stateNode;
// 如果缓存了 且没有变化 直接使用缓存的 context
if (
instance &&
instance.__reactInternalMemoizedUnmaskedChildContext === unmaskedContext
) {
return instance.__reactInternalMemoizedMaskedChildContext;
}
// 按照 key 过滤 unmaskedContext 中的值
const context = {};
for (const key in contextTypes) {
context[key] = unmaskedContext[key];
}
// 缓存到 react 实例上
if (instance) {
cacheContext(workInProgress, unmaskedContext, context);
}
return context;
}
}2.1.6. 旧版本上下文存储
在旧版本的上下文数据存储中其主要依赖以下几个属性 和 堆栈的数据结构存储 整个上下文数据的。
- 在 Fiber 节点上存储 提供者组件节点 当前的两个重要的上下文对象
node.stateNode.__reactInternalMemoizedUnmaskedChildContext用于存储当前组件的未过滤的上下文对象。node.stateNode.__reactInternalMemoizedMaskedChildContext用于存储当前组件的过滤后的上下文对象。
- 通过堆栈的方式存储 FiberNode 数中的所有的 提供者组件节点 的上下文对象
2.2. 新版本上下文能力
React 通过createContext创建一个上下文对象,然后通过Provider组件提供数据,Consumer组件消费数据。
1. React.createContext 创建上下文对象
js
const MyContext = React.createContext(defaultValue);defaultValue:可选的默认值,当没有 Provider 提供数据时使用。
2. Provider 提供数据
js
<MyContext.Provider value={"你要消费我"}>
<App />
</MyContext.Provider>value:要提供给消费组件的数据。
3.1. Consumer 消费数据
js
<MyContext.Consumer>
{
(value) => {
<div>{value}</div>;
} /* 基于 context 值进行渲染*/
}
</MyContext.Consumer>value:从最近的 Provider 中获取到的数据。
3.2. useContext 消费数据
js
function MyApp() {
const value = useContext(MyContext);
return <div>{value}</div>;
}在 React 源码里,createContext 函数在创建新的 Context 对象时发挥着关键作用。它为开发者提供了一种便捷的方式来生成用于数据共享的 Context 实例。下面就为大家呈现简化后的源码示例:
3.3. Class 组件 contextType 消费数据
js
class MyApp extends React.Component {
static contextType = MyContext;
render() {
return <div>{this.context}</div>;
}
}2.3. 重点
为什么要在
updateContainer函数中获取当前组件的上下文对象?而不是像新版本在处理组件 Fiber 的时候获取?新旧版本在 context 数据的存储 和 传输 上有啥区别?
3. 源码解析
3.1. createContext 函数的实现
主要是创建一个 Context 对象,这个对象包含了
- Provider 和 Consumer 组件。
_currentValue用于存储当前的 context 上下文的值。_threadCount用于跟踪当前 context 支持的并发渲染器数量。
js
export function createContext<T>(defaultValue: T): ReactContext<T> {
const context: ReactContext<T> = {
// 节点的类型为 REACT_CONTEXT_TYPE
$$typeof: REACT_CONTEXT_TYPE,
// 保存当前的 context 上下文的值
// 当有两个渲染器时,会有两个值,一个是 primary,一个是 secondary
_currentValue: defaultValue,
_currentValue2: defaultValue,
// Used to track how many concurrent renderers this context currently
// supports within in a single renderer. Such as parallel server rendering.
_threadCount: 0,
// These are circular
// 生产者
Provider: (null: any),
// 消费者
Consumer: (null: any),
// Add these to use same hidden class in VM as ServerContext
_defaultValue: (null: any),
_globalName: (null: any),
};
// 提供一个生产者的组件对象
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};
// 提供一个消费者的对象
context.Consumer = context;
return context;
}3.2. Provider 组件的实现
Provider 组件,他是一个生产者,主要是负责给当前内部的 Consumer 组件提供数据。
其主要也是在 beginWork 阶段,当组件的 type 为 REACT_PROVIDER_TYPE 时,会执行 updateContextProvider 函数。
updateContextProvider 函数
其主要解决以下几个问题
- FunctionComponent 节点中多个 useContext 的情况
js
function MyApp() {
const value = useContext(MyContext);
const value2 = useContext(MyContext2);
return <div>{value}</div>;
}对于这种 MyApp 组件来说,在 useContext 的时候会通过 readContext 函数来读取当前的 context 上下文的值,并将当前的 context 上下文的值添加到当前的 fiber 节点的 dependencies 链表中。
这样 MyApp 组件的 FiberNode 就变成了这样
js
{
tag: FunctionComponent,
type: MyApp,
key: null,
dependencies: {
firstContext: {
context: MyContext,
next: {
context: MyContext2,
next: null
}
}
}
}其实现如下
js
export function readContext<T>(context: ReactContext<T>): T {
// 构建一个 contextItem 对象
// 并将其添加到当前的 fiber 节点的 dependencies 链表中
const contextItem = {
context: ((context: any): ReactContext<mixed>),
memoizedValue: value,
next: null,
};
// 使用全局变量 lastContextDependency 来记录当前的 context 上下文的值
// 如果为 null ,则表明是第一次 useContext ,则将其赋值给 lastContextDependency 并将 dependencies 链表的 firstContext 指向当前的 contextItem
// 如果不为 null ,则直接使用 lastContextDependency.next 来指向当前的 contextItem 即可
if (lastContextDependency === null) {
// This is the first dependency for this component. Create a new list.
lastContextDependency = contextItem;
currentlyRenderingFiber.dependencies = {
lanes: NoLanes,
firstContext: contextItem,
};
if (enableLazyContextPropagation) {
currentlyRenderingFiber.flags |= NeedsPropagation;
}
} else {
// Append a new context item.
lastContextDependency = lastContextDependency.next = contextItem;
}
}那么通过循环遍历 dependencies 链表,就可以找到函数式组件中订阅的所有的 context 上下文的值了。
- 相同 Provider 节点的嵌套 作用域问题
问题
js
function MyApp() {
return (
<MyConText.Provider value={1}>
<MyConText.Provider value={2}>
<MyConText.Consumer>
{(value) => {
return <div>{value}</div>; // 2
}}
</MyConText.Consumer>
</MyConText.Provider>
<MyConText.Consumer>
{(value) => {
return <div>{value}</div>; // 1
}}
</MyConText.Consumer>
</MyConText.Provider>
);
}对于这种情况来说, 在内部的 Provider 节点中,其 Consumer 节点消费的应该是内部的 Provider 节点提供的值,而不是外部的 Provider 节点提供的值。那么如何解决的呐?
通过栈地方式去维护当前的 context 上下文的值。在 <MyConText.Provider>(内部)组件的时候通过 pushProvider(workInProgress, context, newValue); 将历史值 1 压入到栈中,并将 context._currentValue 的值修改成最新值 2 , 在 completeWork 阶段通过 popProvider(workInProgress, context); 将栈中的值 1 弹出,并将 context._currentValue 的值修改成 1。
这样就保证了内部作用域的隔离
- 订阅者 Consumer 节点的通知更新
通过深度优先的方式去遍历当前 Provider 节点的子孙节点,找到对应的订阅者节点,通过创建 update 对象 或者 标记 lane 的方式,来触发订阅者的更新
其主要方法是 propagateContextChange_eager 函数
propagateContextChange_eager 函数
源码
js
/**
* 通过深度优先的方式去遍历当前 Provider节点的子孙节点,找到对应的订阅者节点。
* 1. FunctionComponent 节点中多个 useContext的情况
* 2. 相同Provider节点的嵌套 作用域问题
* @param {*} workInProgress
* @param {*} context
* @param {*} renderLanes
* @returns
*/
function propagateContextChange_eager<T>(
workInProgress: Fiber,
context: ReactContext<T>,
renderLanes: Lanes
): void {
// Only used by eager implementation
if (enableLazyContextPropagation) {
return;
}
let fiber = workInProgress.child;
if (fiber !== null) {
// Set the return pointer of the child to the work-in-progress fiber.
fiber.return = workInProgress;
}
while (fiber !== null) {
let nextFiber;
// Visit this fiber.
// 获取存在 dependencies 的 fiber 节点
// 如 FunctionComponent 节点中使用了 useContext
const list = fiber.dependencies;
if (list !== null) {
nextFiber = fiber.child;
// 遍历 dependencies 链表中是否存在当前的 context 即订阅者
// 在函数式组件中 可能使用多个 useContext ,所以是一个链表,
let dependency = list.firstContext;
while (dependency !== null) {
// Check if the context matches.
// 如果找到当前的订阅者
if (dependency.context === context) {
// Match! Schedule an update on this fiber.
// 对于 ClassComponent 节点来说, 生成一个 update 对象并添加到 updateQueue 中
// 对于 FunctionComponent 节点来说, 生成一个 update 对象并添加到 updateQueue 中
if (fiber.tag === ClassComponent) {
// Schedule a force update on the work-in-progress.
const lane = pickArbitraryLane(renderLanes);
const update = createUpdate(NoTimestamp, lane);
update.tag = ForceUpdate;
// TODO: Because we don't have a work-in-progress, this will add the
// update to the current fiber, too, which means it will persist even if
// this render is thrown away. Since it's a race condition, not sure it's
// worth fixing.
// Inlined `enqueueUpdate` to remove interleaved update check
const updateQueue = fiber.updateQueue;
if (updateQueue === null) {
// Only occurs if the fiber has been unmounted.
} else {
const sharedQueue: SharedQueue<any> = (updateQueue: any).shared;
const pending = sharedQueue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
}
}
// 标记 fiber 节点中存在当前的 lane
fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
}
// 向上遍历的方式 将当前Fiber节点的 lane 都添加到 父节点的 childLanes 中
scheduleContextWorkOnParentPath(
fiber.return,
renderLanes,
workInProgress
);
// Mark the updated lanes on the list, too.
list.lanes = mergeLanes(list.lanes, renderLanes);
// Since we already found a match, we can stop traversing the
// dependency list.
break;
}
dependency = dependency.next;
}
} else if (fiber.tag === ContextProvider) {
// Don't scan deeper if this is a matching provider
// 如果遇到 相同的 子Provider 节点, 则停止遍历 应为下面的 Provider 节点会覆盖当前的 Provider 节点
nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
} else if (fiber.tag === DehydratedFragment) {
// If a dehydrated suspense boundary is in this subtree, we don't know
// if it will have any context consumers in it. The best we can do is
// mark it as having updates.
const parentSuspense = fiber.return;
if (parentSuspense === null) {
throw new Error(
"We just came from a parent so we must have had a parent. This is a bug in React."
);
}
parentSuspense.lanes = mergeLanes(parentSuspense.lanes, renderLanes);
const alternate = parentSuspense.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
}
// This is intentionally passing this fiber as the parent
// because we want to schedule this fiber as having work
// on its children. We'll use the childLanes on
// this fiber to indicate that a context has changed.
scheduleContextWorkOnParentPath(
parentSuspense,
renderLanes,
workInProgress
);
nextFiber = fiber.sibling;
} else {
// Traverse down.
nextFiber = fiber.child;
}
if (nextFiber !== null) {
// Set the return pointer of the child to the work-in-progress fiber.
// 将子节点的 return 指向当前节点
nextFiber.return = fiber;
} else {
// No child. Traverse to next sibling.
// 如果没有子节点了,遍历到下一个兄弟节点
nextFiber = fiber;
while (nextFiber !== null) {
if (nextFiber === workInProgress) {
// We're back to the root of this subtree. Exit.
nextFiber = null;
break;
}
const sibling = nextFiber.sibling;
if (sibling !== null) {
// Set the return pointer of the sibling to the work-in-progress fiber.
sibling.return = nextFiber.return;
nextFiber = sibling;
break;
}
// No more siblings. Traverse up.
nextFiber = nextFiber.return;
}
}
fiber = nextFiber;
}
}分析
- 对于 dependencies 链表中 context 的比较来判断是否是当前的订阅者
这个可以看上面的问题 1
通过 创建 update 对象 或者 标记 lane 的方式,来触发订阅者的更新
通过向上遍历的方式 将当前 Fiber 节点的 lane 都添加到 父节点的 childLanes 中
3.3. 订阅者的实现
对于 Context 的订阅者来说,其主要有以下三种方式来获取 context 上下文的值,且对于 React16 之前还提供了 contextTypes 、getChildContext 来获取 context 上下文的值
3.3.1. Consumer 组件的实现
Consumer 组件就是在 createContext 函数中创建的 context.Consumer 对象。其本质就是 context 对象,只是 $$typeof === REACT_CONTEXT_TYPE 。
那么在 beginWork 阶段,当组件的 type 为 REACT_CONTEXT_TYPE 时,会执行 updateContextConsumer 函数。
updateContextConsumer 函数
核心流程
prepareToReadContext为readContext准备环境, 将 dependencies 的 firstContext 置为 nullreadContext读取 context 中的最新的值- 执行渲染函数
render(newValue),返回子节点 - 进入子节点的协调过程
js
/**
* 处理 Context.Consumer 消费组件
* 例子 : <MyContext.Consumer>{value => <div>{value}</div>}</MyContext.Consumer>
* @param {*} current
* @param {*} workInProgress
* @param {*} renderLanes
* @returns
*/
function updateContextConsumer(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
) {
let context: ReactContext<any> = workInProgress.type;
const newProps = workInProgress.pendingProps;
// 获取子节点
const render = newProps.children;
// 为 readContext 准备环境, 将 dependencies 的 firstContext 置为 null
prepareToReadContext(workInProgress, renderLanes);
// 读取 context 中的最新的值
const newValue = readContext(context);
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
}
// 执行渲染函数,返回子节点
newChildren = render(newValue);
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
// 进入子节点的协调过程
reconcileChildren(current, workInProgress, newChildren, renderLanes);
return workInProgress.child;
}prepareToReadContext 函数
作用: 为 readContext 准备环境, 将 dependencies 的 firstContext 置为 null
- 对于 Consumer 组件
其订阅的 Provider 组件只可能是第一个 Provider 组件 ,所以其就是 prepareToReadContext() => readContext(context) => context
- 对于 FunctionComponent 组件
其订阅的 Provider 组件可能是多个 Provider 组件,所以其就是 renderWithHooks 的时候 prepareToReadContext() => useContext()的时候 readContext(context) => `useContext()的时候 readContext(context)
源码
js
/**
* 与 readContext 函数配合使用,用于在组件中处理上下文消费的准备工作
* 1. 将组件的 dependencies 重置为 null
* 2. 判断组件中是否存在当前优先级的更新,如存在强制更新
* 使用场景:
* - 函数组件开始渲染前( renderWithHooks )
* - 类组件执行 render 方法前
* - 上下文消费者组件更新时
* - 并发渲染模式下的分片渲染阶段
* @param {*} workInProgress
* @param {*} renderLanes
*/
export function prepareToReadContext(
workInProgress: Fiber,
renderLanes: Lanes
): void {
currentlyRenderingFiber = workInProgress;
lastContextDependency = null;
lastFullyObservedContext = null;
// 获取组件的 dependencies 链表
const dependencies = workInProgress.dependencies;
if (dependencies !== null) {
if (enableLazyContextPropagation) {
// Reset the work-in-progress list
dependencies.firstContext = null;
} else {
const firstContext = dependencies.firstContext;
if (firstContext !== null) {
// 检查当前渲染是否包含需要处理的车道
if (includesSomeLane(dependencies.lanes, renderLanes)) {
// Context list has a pending update. Mark that this fiber performed work.
markWorkInProgressReceivedUpdate();
}
// Reset the work-in-progress list
dependencies.firstContext = null;
}
}
}
}readContext 函数
作用: 生成一个 contextItem 对象,用于存储上下文的依赖关系, 并将其存储到 dependencies 链表中
源码
js
/**
* 读取上下文的 context 中的值
* @param {*} context
* @returns
*/
export function readContext<T>(context: ReactContext<T>): T {
// 获取 Context中当前的值
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
if (lastFullyObservedContext === context) {
// Nothing to do. We already observe everything in this context.
} else {
// 创建一个 contextItem 对象,用于存储上下文的依赖关系
const contextItem = {
// context 对象
context: ((context: any): ReactContext<mixed>),
// 当前的值
memoizedValue: value,
// 下一个 contextItem 对象
next: null,
};
// 记录当前的 contextItem 对象
// 如果 lastContextDependency 为 null,说明当前组件的 dependencies 链表为空
// 则将当前的 contextItem 对象作为第一个节点
// 如果 lastContextDependency 不为 null,说明当前组件的 dependencies 链表不为空
// 则将当前的 contextItem 对象作为最后一个节点的 next 属性
if (lastContextDependency === null) {
if (currentlyRenderingFiber === null) {
throw new Error();
}
// This is the first dependency for this component. Create a new list.
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;
}3.3.2. useContext 函数的实现
useContext 函数是 React 18 中新增的一个 Hook 函数,用于在函数组件中读取上下文的值。其用在 FunctionComponent 组件中。
主要流程
- 在
updateFunctionComponent函数中,会执行prepareToReadContext函数,将 dependencies 的 firstContext 置为 null - 在
useContext函数中,就是执行readContext函数,生成一个 contextItem 对象,用于存储上下文的依赖关系, 并将其存储到 dependencies 链表中
js
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useContext: readContext,
};3.3.3. Class 组件 contextType 的实现
js
function updateClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes
) {
// Push context providers early to prevent context stack mismatches.
// During mounting we don't know the child context yet as the instance doesn't exist.
// We will invalidate the child context in finishClassComponent() right after rendering.
let hasContext;
if (isLegacyContextProvider(Component)) {
hasContext = true;
pushLegacyContextProvider(workInProgress);
} else {
hasContext = false;
}
//
prepareToReadContext(workInProgress, renderLanes);
// 获取 class 的实例对象
const instance = workInProgress.stateNode;
let shouldUpdate;
// 首次渲染
if (instance === null) {
// In the initial pass we might need to construct the instance.
constructClassInstance(workInProgress, Component, nextProps);
}
return nextUnitOfWork;
}
function constructClassInstance(
workInProgress: Fiber,
ctor: any,
props: any
): any {
let isLegacyContextConsumer = false;
let unmaskedContext = emptyContextObject;
let context = emptyContextObject;
// 获取 class 对象上的 contextType 属性
const contextType = ctor.contextType;
// 1. 获取初始化的时候传入的 context 值
//
if (typeof contextType === "object" && contextType !== null) {
context = readContext((contextType: any));
} else if (!disableLegacyContext) {
unmaskedContext = getUnmaskedContext(workInProgress, ctor, true);
const contextTypes = ctor.contextTypes;
isLegacyContextConsumer =
contextTypes !== null && contextTypes !== undefined;
context = isLegacyContextConsumer
? getMaskedContext(workInProgress, unmaskedContext)
: emptyContextObject;
}
return instance;
}3.3.4 Class 组件 contextTypes 的实现
这是 React16 之前定义 context 上下文的方式
其没有通过 Class.contextType 去指定获取哪一个上下文的内容,而是直接通过 contextStackCursor.current 去访问 context 栈中最新的值的内容, 然后通过 Class.contextTypes 去过滤只获取哪些属性的内容。所以其对于上下文 context 的值是通过组件所在位置来决定,而不是由组件本身来决定。
对于这种方式,在后面通过 Class.contextType 去显示的指定获取哪一个上下文的内容。
源码
新旧版上下文判断
js
/**
* 是否支持且存在旧版上下文逻辑
* childContextTypes 是否存在
* @param {*} type
* @returns
*/
function isContextProvider(type: Function): boolean {
if (disableLegacyContext) {
return false;
} else {
const childContextTypes = type.childContextTypes;
return childContextTypes !== null && childContextTypes !== undefined;
}
}通过判断当前版本是否禁用旧版上下文逻辑,和 Class 组件本身是否定义了 childContextTypes 属性来判断是否支持旧版上下文逻辑。
上下文值的获取
js
// 1. 获取初始化的时候传入的 context 值
if (typeof contextType === "object" && contextType !== null) {
// 读取 Class.contextType 传入的 context 对象
context = readContext((contextType: any));
} else if (!disableLegacyContext) {
// 获取未处理的 context 上下文的值
unmaskedContext = getUnmaskedContext(workInProgress, ctor, true);
// 是否定义了 contextTypes 属性
const contextTypes = ctor.contextTypes;
isLegacyContextConsumer = contextTypes !== null && contextTypes !== undefined;
// 如果定义了 contextTypes 属性, 则将 unmaskedContext 转换 按照 contextTypes 进行过滤
// 否则直接返回空对象
context = isLegacyContextConsumer
? getMaskedContext(workInProgress, unmaskedContext)
: emptyContextObject;
}- 优先根据 Class.contextType 传入的 context 对象来获取 context 值, 如果定义了则直接不对旧版上下文逻辑进行处理
- 如果没有定义 Class.contextType
getUnmaskedContext(workInProgress, ctor, true)获取当前 context 栈中最新的上下文内容- 根据是否定义 Class.contextTypes 来判断是否需要过滤上下文内容,即
getMaskedContext(workInProgress, unmaskedContext)
getUnmaskedContext 函数
对于旧版上下文逻辑其主要通过直接获取 contextStackCursor.current 的值来获取 context 上下文对象。这个值可以在 pushProvider 方法中看到
js
/**
* 获取未过滤的 context 的值
* 获取的是 contextStackCursor.current 的值
* @param {*} workInProgress
* @param {*} Component
* @param {*} didPushOwnContextIfProvider
* @returns
*/
function getUnmaskedContext(
workInProgress: Fiber,
Component: Function,
didPushOwnContextIfProvider: boolean
): Object {
// 禁用旧版 contextTypes 则返回空对象
if (disableLegacyContext) {
return emptyContextObject;
} else {
if (didPushOwnContextIfProvider && isContextProvider(Component)) {
// If the fiber is a context provider itself, when we read its context
// we may have already pushed its own child context on the stack. A context
// provider should not "see" its own child context. Therefore we read the
// previous (parent) context instead for a context provider.
return previousContext;
}
// 返回 contextStackCursor.current 的值
return contextStackCursor.current;
}
}getMaskedContext 函数
按照 contextTypes 的值对 unmaskedContext 进行过滤
js
/**
* 获取按照 contextTypes 过滤后的 context 对象
* 如果没有定义 contextTypes 则返回空对象
* 如果定义了 contextTypes 则按照 key 过滤 unmaskedContext 中的值
* 并返回过滤后的 context 对象
* 同时缓存到 react 的 instance.__reactInternalMemoizedUnmaskedChildContext 实例上
* @param {*} workInProgress
* @param {*} unmaskedContext
* @returns
*/
function getMaskedContext(
workInProgress: Fiber,
unmaskedContext: Object
): Object {
if (disableLegacyContext) {
return emptyContextObject;
} else {
const type = workInProgress.type;
// 获取组件上定义的 contextTypes 主要是 React16之前的方式
const contextTypes = type.contextTypes;
// 如果没有定义 contextTypes 则直接 { }
if (!contextTypes) {
return emptyContextObject;
}
// Avoid recreating masked context unless unmasked context has changed.
// Failing to do this will result in unnecessary calls to componentWillReceiveProps.
// This may trigger infinite loops if componentWillReceiveProps calls setState.
const instance = workInProgress.stateNode;
// 如果缓存了 且没有变化 直接使用缓存的 context
if (
instance &&
instance.__reactInternalMemoizedUnmaskedChildContext === unmaskedContext
) {
return instance.__reactInternalMemoizedMaskedChildContext;
}
// 按照 key 过滤 unmaskedContext 中的值
const context = {};
for (const key in contextTypes) {
context[key] = unmaskedContext[key];
}
// 缓存到 react 实例上
if (instance) {
cacheContext(workInProgress, unmaskedContext, context);
}
return context;
}
}