Context的实现流程
useContext的实现
本系列是讲述从0开始实现一个react18的基本版本。通过实现一个基本版本,让大家深入了解React
内部机制。
由于React
源码通过Mono-repo 管理仓库,我们也是用pnpm
提供的workspaces
来管理我们的代码仓库,打包我们使用rollup
进行打包。
系列文章:
- React实现系列一 - jsx
- 剖析React系列二-reconciler
- 剖析React系列三-打标记
- 剖析React系列四-commit
- 剖析React系列五-update流程
- 剖析React系列六-dispatch update流程
- 剖析React系列七-事件系统
- 剖析React系列八-同级节点diff
- 剖析React系列九-Fragment的部分实现逻辑
- 剖析React系列十- 调度<合并更新、优先级>
- 剖析React系列十一- useEffect的实现原理
- 剖析React系列十二-调度器的实现
- 剖析React系列十三-react调度
- useTransition的实现
- useRef的实现
前面我们已经讲了useRef
的实现,这节我们来讲一下useContext
的实现。本节我们只针对函数组件的使用进行分析和实现。
1. Context的基本使用
当我们需要跨组件传递数据的时候,通常我们会使用context
来进行传递。context
的使用方式分为下面2个步骤:
- 创建
context
对象const context = React.createContext(defaultValue)
- 使用
context
对象的Provider
组件进行包裹,<context.Provider value={value}>
,value
就是我们需要传递的数据 - 在需要使用
value
的地方,调用useContext
函数,const value = useContext(context)
// 1. 创建context对象
const ctx = createContext(null);
// 2. 使用context对象的Provider组件进行包裹
<ctx.Provider value={num}>
<div onClick={() => update(Math.random())}>
<Middle />
</div>
</ctx.Provider>
// 3. 在需要使用value的地方,调用useContext函数
function Child() {
const val = useContext(ctx);
return <p>{val}</p>;
}
所以我们需要实现如下的几个点:
createContext
函数Provider
组件useContext
函数
2. createContext函数
createContext
函数的作用是创建一个context
对象,这个对象包含了Provider
组件和Consumer
组件。但是我们目前不考虑Consumer
, 我们先来看一下createContext
函数的返回情况:
Provider
组件_currentValue
: 保存Provider
组件的value
值$$typeof
: 用来标记这个对象是一个context
对象
export function createContext<T>(defaultValue: T): ReactContext<T> {
const context: ReactContext<T> = {
$$typeof: REACT_CONTEXT_TYPE,
Provider: null,
_currentValue: defaultValue,
};
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};
return context;
}
3. ctx.Provider
组件的编译过程
当我们写了<ctx.Provider value={num}>
的时候,在开发环境babel
会将其编译成如下的代码:
jsxDEV(ctx.Provider, { value: num, children: /* @__PURE__ */ jsxDEV("div", { onClick: () => update(Math.random()), children: 'xxxx'})});
所以当执行到jsxDEV(ctx.Provider,xxx)
的时候, 基于之前的jsxDEV
的逻辑,我们会得到如下的ReactElement
的结构。
let ctx_Provider_ReactElement = {
$$typeof: REACT_ELEMENT_TYPE,
type: {
$$typeof:Symbol(react.provider),
_context: 'xxxx'
},
props: {
value: 0,
children: {
xxxx
}
},
xxxx
}
3.1 beginWork基于父节点处理新的类型
之前我们在处理beginWork
进行调和的时候,我们已经知道我们会根据相应的reactElement
生成相应的fiber
节点,所以ctx.Provider
的父亲节点开始beginWork
的时候,
会根据ReactElement
的type
进行生成子节点的fiber
节点,但是这里ctx.Provider
的type
是一个对象,所以我们需要对type
进行特殊的处理。
export function createFiberFromElement(element: ReactElementType): FiberNode {
// xxxxxx
if (
// <Context.Provider/>
typeof type === "object" &&
type.$$typeof === REACT_PROVIDER_TYPE
) {
fiberTag = ContextProvider;
}
// xxxxxx
}
这里我们就根据ReactElement
的type
生成了ctx.Provider
对应的fiber
。
3.2 ctx.Provider
对应的调和过程
当我们在beginWork
的时候,遍历到ctx.Provider
的fiber的时候,我们需要根据特定的tag
进行不同的处理,这里我们需要根据tag
进行不同的处理。
export const beginWork = (wip: FiberNode, renderLane: Lane) => {
switch (wip.tag) {
case ContextProvider:
return updateContextProvider(wip);
}
}
在updateContextProvider
中,我们除了要继续调和子节点之外,还需要进行一些特定的逻辑。
3.3 context当前值的入栈与出栈
当我们使用context
的时候,会出现嵌套使用的情况,取值获取最近的Provider
的value
值。所以我们需要将value
值进行入栈和出栈的操作。
// 例如这个嵌套的例子
const ctx = createContext(null);
function App() {
return (
<ctx.Provider value={0}>
<Child />
<ctx.Provider value={1}>
<Child />
<ctx.Provider value={2}>
<Child />
</ctx.Provider>
</ctx.Provider>
</ctx.Provider>
);
}
function Child() {
const val = useContext(ctx);
return <p>{val}</p>;
}
三个不同的child
组件,分别获取到了不同的value
值,这样是怎么操作的。
目前如果实现简单的功能来说,我们只需要在调和的时候,将value
值保存到当前context
的_currentValue
中。
在子节点调和阶段, 执行useContext
传递的context
,然后去获取_currentValue
的值,这样就可以获取到最近的Provider
的value
值。
但是React
的原生中,为了更复杂的场景,他使用了一个数组模拟出栈、入栈的操作,这样可以更好的处理复杂的场景。
import { ReactContext } from "shared/ReactTypes";
const valueStack: any[] = [];
export function pushProvider<T>(context: ReactContext<T>, newValue: T) {
valueStack.push(newValue);
context._currentValue = newValue;
}
export function popProvider<T>(context: ReactContext<T>) {
context._currentValue = valueStack[valueStack.length - 1];
valueStack.pop();
}
updateContextProvider
的细节实现
它和其他的beginWork
的递归子节点实现基本一致,只是多了一些context
的处理, 主要是需要保存当前的context
的值。
当然还有一些优化的逻辑,这个之后再进行补充,需要对比新旧2个值是否发生变化,判断是否需要继续向下调和。
function updateContextProvider(wip: FiberNode) {
const providerType = wip.type;
// {
// $$typeof: symbol | number;
// _context: ReactContext<T>;
// }
const context = providerType._context;
const oldProps = wip.memoizedProps; // 旧的props <Context.Provider value={0}> {value, children}
const newProps = wip.pendingProps;
const newValue = newProps.value; // 新的value
if (oldProps && newValue !== oldProps.value) {
// context.value发生了变化 向下遍历找到消费的context
// todo: 从Provider向下DFS,寻找消费了当前变化的context的consumer
// 如果找到consumer, 从consumer开始向上遍历到Provider
// 标记沿途的组件存在更新
}
// 逻辑 - context入栈
if (__DEV__ && !("value" in newProps)) {
console.warn("<Context.Provider>需要传入value");
}
pushProvider(context, newValue);
const nextChildren = wip.pendingProps.children;
reconcileChildren(wip, nextChildren);
return wip.child;
}
useContext
的实现
useContext
的实现就是获取当前context
的_currentValue
的值
function readContext<T>(context: ReactContext<T>) {
const consumer = currentlyRenderingFiber;
if (consumer === null) {
throw new Error("context需要有consumer");
}
const value = context._currentValue;
return value;
}
总结
这就是context
的传递的整个过程,当然还有一些细节的处理,这里就不一一展开了,这里只是简单的实现了context
的传递的整个过程。
转载自:https://juejin.cn/post/7245178550126854200