说明#
- 本文重点将会分析 React 内的 Context 机制,分为三个小阶段
- createContext
- context.Provider
- useContext
- demo 基于官方例子
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
createContext#
直接对 createContext 打上断点,
export function createContext<T>(defaultValue: T): ReactContext<T> {
/**
* 声明了一个 context
*/
const context: ReactContext<T> = {
$$typeof: REACT_CONTEXT_TYPE,
_currentValue: defaultValue,
_currentValue2: defaultValue,
_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),
};
/**
* beginWork 创建 fiber 时会根据 $$typeof 来分别处理
*/
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};
let hasWarnedAboutUsingNestedContextConsumers = false;
let hasWarnedAboutUsingConsumerProvider = false;
let hasWarnedAboutDisplayNameOnConsumer = false;
context.Consumer = context;
return context;
}
东西不多,内部主要是声明了一个 context 对象,并在上面挂上了 Provider 的属性。
接下来看看 provider 里面是怎么处理的
context.Provider#
仔细看 demo 里面用到 Provider 的地方,能发现它是被以 jsx 的方式使用到程序内的。
那么调试的思路也很明显了,去观察 beginWork 内生成 fiber 的地方
对 App 的 return 打点,会进入以下调用栈
在 createFiberFromTypeAndProps 内会先将 ContextProvider 赋值给 fiberTag,以表示我需要创建一个 Provider 特供的 fiber 节点,然后进入 createFiber,随后便返回了一个 type 为 ContextProvider 的 fiber 节点。
需要注意的是此时我们还在 App 的 beginWork 内,如果需要观察 ContextProvider 的,需要进入下一次 beginWork。
这里才是正式处理 ContextProvider 的地方。
function updateContextProvider(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
const providerType: ReactProviderType<any> = workInProgress.type;
// createContext 创建的 context 对象
const context: ReactContext<any> = providerType._context;
const newProps = workInProgress.pendingProps;
const oldProps = workInProgress.memoizedProps;
const newValue = newProps.value;
// 这里修改了 context 的 _currentValue为新值
pushProvider(workInProgress, context, newValue);
if (oldProps !== null) {
const oldValue = oldProps.value;
// provider 比较 value前后是否发生变化
if (is(oldValue, newValue)) {
// No change. Bailout early if children are the same.
// 没有改变的话,并且 children 未更新就复用原来的 fiber 然后退出
if (
oldProps.children === newProps.children &&
!hasLegacyContextChanged()
) {
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
}
} else {
// 如果前后 value 不同,那么就更新所有 consumers
// 这里暂时先不展开,等到 useContext 时再介绍
// The context value changed. Search for matching consumers and schedule
// them to update.
propagateContextChange(workInProgress, context, renderLanes);
}
}
// 继续创建 provider 的子节点
const newChildren = newProps.children;
reconcileChildren(current, workInProgress, newChildren, renderLanes);
return workInProgress.child;
}
updateContextProvider 的思路也比较简单,修改 context 内的 value 后比较新旧值是否相等,相等就复用 fiber 节点,不相等就重新创建新的 fiber 节点。唯独需要注意的地方是 propagateContextChange。这个我们会结合 useContext 一块讲。
useContext#
令人高兴的是,这个 hook 并不需要分 mount 与 update 来讲。并且也不会生成 hook 对象。
我们直接来看 readContext 里面做了什么就行了。
因为使用这个 hook 时需要传入对应的 context 对象
const theme = useContext(ThemeContext);
export function readContext<T>(context: ReactContext<T>): T {
/**
* 获取 context 传入的 value
* */
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
if (lastFullyObservedContext === context) {
// Nothing to do. We already observe everything in this context.
} else {
// 创建了一个 contextItem,后面会穿成链表挂在当前正在渲染的 fiber 节点上
const contextItem = {
context: ((context: any): ReactContext<mixed>),
memoizedValue: value,
next: null,
};
if (lastContextDependency === null) {
// 创建链表,并挂载在 fiber 节点上
// 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.
// 在 fiber 上的 context item 链表上加元素
lastContextDependency = lastContextDependency.next = contextItem;
}
}
// 把值丢出去
return value;
}
此时大致上就是这么个结构了
readContext 主要做的还是读取传入 context 内的值,但是为什么要在当前渲染的 fiber 节点上增加一个与 context 相关的链表呢?
这时需要回过头看看 propagateContextChange 了
function propagateContextChange_eager(workInProgress, context, renderLanes) {
var 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) {
var nextFiber = void 0; // Visit this fiber.
/**
* fiber 节点上的 dependencies 在这里被视为了内部存在使用了 context 的标识
*/
var list = fiber.dependencies;
if (list !== null) {
// 这里是有使用到 context 的场景
nextFiber = fiber.child;
var dependency = list.firstContext;
/**
* 这里通过 while 遍历 context item 的链表确认是否存在当前 provider 上绑定的context 对象
*/
while (dependency !== null) {
// Check if the context matches.
if (dependency.context === context) {
// 找到了有使用到当前 provider 上标记的 context 对象
// Match! Schedule an update on this fiber.
if (fiber.tag === ClassComponent) {
...
}
// 给当前 fiber 标记上更新标识
fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
var alternate = fiber.alternate;
// 同样也需要给当前 WIP 对应的 current 节点也打上相同的标记,防止被更高优先级任务插队后丢失信息
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
}
/**
* 这里在别的文章里也多次提到过类似的操作(比如 useState)
* 其实就是模拟了生成了一个 renderlanes 的 update
* 将更新从当前节点一直向上标记,直到 root 节点。
* 从而在调度时一起更新
*/
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) {
...
} else if (fiber.tag === DehydratedFragment) {
...
} else {
...
}
...
}
fiber = nextFiber;
}
}
大致总结一下便是,每一个使用到了 context 对象的 fiber 节点都会被打上 dependencies 的链表属性,一旦 provider 上的 value 发生改变,它就会遍历自己的所有子节点是否拥有 dependencies 的标记,并给他们打上更新标记从而实现强制更新。就像文档里说的一样。