likes
comments
collection
share

Context的实现流程

作者站长头像
站长
· 阅读数 14

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的使用方式分为下面2个步骤:

  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, 我们先来看一下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;
}

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
}

Context的实现流程

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

之前我们在处理beginWork进行调和的时候,我们已经知道我们会根据相应的reactElement生成相应的fiber节点,所以ctx.Provider的父亲节点开始beginWork的时候, 会根据ReactElementtype进行生成子节点的fiber节点,但是这里ctx.Providertype是一个对象,所以我们需要对type进行特殊的处理。

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对应的调和过程

当我们在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的时候,会出现嵌套使用的情况,取值获取最近的Providervalue值。所以我们需要将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的值,这样就可以获取到最近的Providervalue值。

但是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的传递的整个过程。 Context的实现流程