banner
IWSR

IWSR

我永远喜欢志喜屋梦子!

React Hooks: useContext & Context

说明#

  1. 本文重点将会分析 React 内的 Context 机制,分为三个小阶段
    1. createContext
    2. context.Provider
    3. useContext
  2. 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 的属性。

image

接下来看看 provider 里面是怎么处理的

context.Provider#

仔细看 demo 里面用到 Provider 的地方,能发现它是被以 jsx 的方式使用到程序内的。

那么调试的思路也很明显了,去观察 beginWork 内生成 fiber 的地方

对 App 的 return 打点,会进入以下调用栈

image

在 createFiberFromTypeAndProps 内会先将 ContextProvider 赋值给 fiberTag,以表示我需要创建一个 Provider 特供的 fiber 节点,然后进入 createFiber,随后便返回了一个 type 为 ContextProvider 的 fiber 节点。

image

需要注意的是此时我们还在 App 的 beginWork 内,如果需要观察 ContextProvider 的,需要进入下一次 beginWork。

image

这里才是正式处理 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 对象。

image

我们直接来看 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;
}

此时大致上就是这么个结构了

image

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;
    }
  }

image

大致总结一下便是,每一个使用到了 context 对象的 fiber 节点都会被打上 dependencies 的链表属性,一旦 provider 上的 value 发生改变,它就会遍历自己的所有子节点是否拥有 dependencies 的标记,并给他们打上更新标记从而实现强制更新。就像文档里说的一样。

image

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。