Context的实现流程

275 阅读4分钟

useContext的实现

本系列是讲述从0开始实现一个react18的基本版本。通过实现一个基本版本,让大家深入了解React内部机制。 由于React源码通过Mono-repo 管理仓库,我们也是用pnpm提供的workspaces来管理我们的代码仓库,打包我们使用rollup进行打包。

仓库地址

本节对应的代码

系列文章:

  1. React实现系列一 - jsx
  2. 剖析React系列二-reconciler
  3. 剖析React系列三-打标记
  4. 剖析React系列四-commit
  5. 剖析React系列五-update流程
  6. 剖析React系列六-dispatch update流程
  7. 剖析React系列七-事件系统
  8. 剖析React系列八-同级节点diff
  9. 剖析React系列九-Fragment的部分实现逻辑
  10. 剖析React系列十- 调度<合并更新、优先级>
  11. 剖析React系列十一- useEffect的实现原理
  12. 剖析React系列十二-调度器的实现
  13. 剖析React系列十三-react调度
  14. useTransition的实现
  15. useRef的实现

前面我们已经讲了useRef的实现,这节我们来讲一下useContext的实现。本节我们只针对函数组件的使用进行分析和实现。

1. Context的基本使用

当我们需要跨组件传递数据的时候,通常我们会使用context来进行传递。context的使用方式分为下面几个步骤:

  1. 创建context对象const context = React.createContext(defaultValue)
  2. 使用context对象的Provider组件进行包裹,<context.Provider value={value}>value就是我们需要传递的数据
  3. 在需要使用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>;
}

所以我们需要实现如下的几个点:

  1. createContext函数
  2. Provider组件
  3. useContext函数

2. createContext函数

createContext函数的作用是创建一个context对象,这个对象包含了Provider组件和Consumer组件。在函数组件中,我们很少使用Consumer,所以我们目前不考虑Consumer。 我们先来看一下大致的createContext函数的返回情况:

  1. Provider组件
  2. _currentValue: 保存Provider组件的value
  3. $$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;
}

对应的结构如下图: 企业微信截图_16862968844730.png

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
}

其中$$typeof标记它是一个ReactElement元素,type是一个对象,表示Context.provier

3.1 beginWork基于父节点处理新的类型

之前我们在处理beginWork进行调和的时候,我们已经知道我们会根据相应的reactElement生成相应的fiber节点,所以ctx.Provider的父亲节点开始beginWork的时候。

会根据ReactElementtype进行生成子节点的fiber节点,这里ctx.Providertype是一个对象,所以我们需要对type进行特殊的处理。然后标记对应的fiber节点

export function createFiberFromElement(element: ReactElementType): FiberNode {
  // xxxxxx
  if (
    // <Context.Provider/>
    typeof type === "object" &&
    type.$$typeof === REACT_PROVIDER_TYPE
  ) {
    fiberTag = ContextProvider;
  }
  // xxxxxx
}

这里我们就根据ReactElementtype生成了ctx.Provider对应的fiber

3.2 ctx.Provider对应的调和过程

调和过程主要分为2部分,beginWorkcompleteWork,类似于栈行为,先进后出。

当我们在beginWork的时候,遍历到ctx.Provider的fiber的时候,我们需要根据特定的fiber tag进行不同的处理,这里我们需要根据tag进行不同的处理。

export const beginWork = (wip: FiberNode, renderLane: Lane) => {
  switch (wip.tag) {
    case ContextProvider:
      return updateContextProvider(wip);
  }
}

updateContextProvider中,我们除了要继续调和子节点之外,还需要进行一些特定的逻辑,保证Context的顺序。

3.2.1 context当前值的入栈与出栈

当我们使用context的时候,会出现嵌套使用的情况,Context的取值是获取最近的Providervalue值。所以我们需要将value值按照一定的顺序储存下来,类似于入栈和出栈的操作。

下面举一个实际的例子情况:在同一个Context出现嵌套的情况

// 例如这个嵌套的例子
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(0,1,2)值,这样是怎么操作的。

目前如果实现简单的功能来说,我们只需要在调和的时候,将value值保存到当前context_currentValue中。

在子节点调和阶段, 执行useContext传递的context,然后去获取_currentValue的值,这样就可以获取到最近的Providervalue值。

但是React的原生中,为了更复杂的场景,他使用了一个数组模拟出栈、入栈的操作,这样可以更好的处理复杂的场景。

import { ReactContext } from "shared/ReactTypes";

const prevContextValue = null
const prevContextValueStack: any[] = [];

export function pushProvider<T>(context: ReactContext<T>, newValue: T) {
  prevContextValueStack.push(prevContextValue);
  prevContextValue = context._currentValue
  context._currentValue = newValue;
}

export function popProvider<T>(context: ReactContext<T>) {
  context._currentValue = preContextValue 
  prevContextValue = prevContextValueStack.pop()
}

短短的几行代码,就是Context值传递的原理。

其中pushProvider是在beginWork中将值传入。popProvider是在completeWork中完成了自己节点的调和的时候,将值赋值为上一个ContextValue的值。

3.3 updateContextProvider的细节实现

Provider的fiber处理和其他的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
  
  // TODO:
  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;
}

3.4 completeWork的处理

Provider节点到了completeWork阶段的时候,需要调用popProvider_currentValue赋值为上一个的值。

export const completeWork = (wip: FiberNode) => {
    const newProps = wip.pendingProps;
    const current = wip.alternate;

    switch (wip.tag) {
        case ContextProvider:
          const context = wip.type._context;
          popProvider(context);
          bubbleProperties(wip);
          return null;
    }

}

3.5 useContext的实现

useContext的实现就是获取当前context_currentValue的值,这个比较简单,就是我们传递进去的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;
}

4 例子:

可能有人会想#### 3.2.1 context当前值的入栈与出栈短短几行代码,当遇到不同的context的嵌套的时候,会不会出现值的顺序不对的情况。接下来,我们通过实际的例子,来看看具体的流程。

const ctxA = createContext("deafult A");
const ctxB = createContext("default B");

function App() {
  return (
    <ctxA.Provider value={"A0"}>
      <ctxB.Provider value={"B0"}>
        <ctxA.Provider value={"A1"}>
          <Cpn />
        </ctxA.Provider>
      </ctxB.Provider>
      <Cpn />
    </ctxA.Provider>
  );
}

function Cpn() {
  const a = useContext(ctxA);
  const b = useContext(ctxB);
  return (
    <div>
      A: {a} B: {b}
    </div>
  );
}

在这个例子中,我们有2个ContextctxActxB)分别嵌套,我们来看看实际的执行流程。 主要是有三个变量:

  1. prevContextValueStack
  2. prevContextValue
  3. _currentValue

在beginWork阶段:

beginWork阶段.png

在completeWork阶段:

completeWork.png

通过beginWorkcompleteWork完整的过程后,最后又重置成初始化状态。

正是这种类似于进栈和出栈的过程,刚刚好可以满足我们context就近的值的情况。

总结

这就是context的传递的整个过程,当然还有一些细节的处理,这里就不一一展开了,这里只是简单的实现了context的传递的整个过程。 context.png