[React 源码] useContext [1.3k 字 - 阅读时长3min]

156 阅读1分钟

代码都来自 React 18 源码, 大家可以放心食用

读完收获

当我们 React.creataContext() 时 发生了什么?

Context.Provider 是如何挂载与更新的?

useContext 是怎么样获取到上下文对象的?

从下面的代码开始入手分析,下面代码完成的是点击文字后随机修改文字颜色。

import * as React from "react";
import { createContext, useState, useContext } from "react";
import ReactDOM from "react-dom";

const genColor = () => `hsla(${Math.random() * 360}, 70%, 50%)`;

type MyContext = { color: string, changer: (any) => void };

const ThemeContext = createContext < MyContext > null;

const Comp = () => {
  const { color, changer } = useContext(ThemeContext);

  return (
    <button style={{ color, fontSize: "2em" }} onClick={changer}>
      Hello World! Click to change color
    </button>
  );
};

const App = () => {
  const [state, setState] = useState({ color: "green" });

  const { color } = state;
  const changer = () => setState({ color: genColor() });

  return (
    <ThemeContext.Provider value={{ color, changer }}>
      <Comp />
    </ThemeContext.Provider>
  );
};

Mount 阶段

第一步:首先通过 createContext 创建了 context 对象。我们传入的 defaultValue 会被当作 context 对象的 __currentValue 属性,同时 我们也可以发现 context.Provider 是一个 React 元素。 Provider 的 __context 属性指向了 context 对象。

export function createContext<T>(defaultValue: T): ReactContext<T> {
  const context: ReactContext<T> = {
    $$typeof: REACT_CONTEXT_TYPE,
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    _threadCount: 0,
    Provider: (null: any),
    Consumer: (null: any),
    _defaultValue: (null: any),
    _globalName: (null: any),
  };
  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };
  context.Consumer = context;
  return context;
}

第二步:这里我们默认 reconciler 阶段 深度优先调度遍历到了 App Fiber 节点,创建了颜色状态,改变颜色状态的函数。传入 Context.Provider 组件的 props 当中。

  const [state, setState] = useState({ color: "green" });

  const { color } = state;
  const changer = () => setState({ color: genColor() });
    return (
    <ThemeContext.Provider value={{ color, changer }}>
      <Comp />
    </ThemeContext.Provider>
  );

第三步:默认走到 reconciler 阶段, beginWork 函数中开始挂载 Context.Provider,调用updateContextProvider 函数

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {
  switch (workInProgress.tag) {
    case ContextProvider:
      return updateContextProvider(current, workInProgress, renderLanes);
  }
}

第四步:updateContextProvider 函数当中调用 pushProvider 函数。

function updateContextProvider(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
) {

  /* 这里的 workInProgress.type 就是 Provider.type = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  }; */
  const providerType: ReactProviderType<any> = workInProgress.type;
 // providerType._context; 就是 Provider.type.context
  const context: ReactContext<any> = providerType._context;
  const newProps = workInProgress.pendingProps;
  const oldProps = workInProgress.memoizedProps;
  const newValue = newProps.value;
  pushProvider(workInProgress, context, newValue);
  
  const newChildren = newProps.children;
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}

let hasWarnedAboutUsingContextAsConsumer = false;

第五步: pushProvider 函数当中将 newValue 也就是 <ThemeContext.Provider value={{ color, changer }}> 的 props 值 赋值给 context.__currentValue。现在 context.__currentValue = { color, changer }

export function pushProvider<T>(
  providerFiber: Fiber,
  context: ReactContext<T>,
  nextValue: T
): void {
  context._currentValue = nextValue;
  context._currentValue2 = nextValue;
}

第六步:默认 reconciler 阶段深度优先调度遍历到了 Comp Fiber 节点, beginWork 函数判断 workInProgress.tag根据 tag 进行挂载,函数式组件调用 renderWithHooks,在 renderWithHooks 函数当中 调用 Comp 函数。继续调用 useContext(ThemeContext)

const Comp = () => {
  const { color, changer } = useContext(ThemeContext);

  return (
    <button style={{ color, fontSize: "2em" }} onClick={changer}>
      Hello World! Click to change color
    </button>
  );
};

第七步:调用 useContext 并传入 ThemeContext,返回 context.__currentVale, 由于第五步挂载 <Context.Provider/> 的时候,已经将 context.__currntValue 修改成了 {color:"green", changer: () => setState(randomColor)},所以该对象被返回。

function useContext<T>(context: ReactContext<T>): T {
  return context._currentValue;
}

第八步:Comp 返回新的虚拟 Dom,去 reconcilChildren…… reconciler 阶段结束 -> render 阶段完成之后,真实 Dom 挂载,文字颜色就是上下文对象提供的 color。

return (
    <button style={{ color, fontSize: "2em" }} onClick={changer}>
      Hello World! Click to change color
    </button>
  );

自此 Context 挂载阶段完成。


Update 阶段

第一步:点击按钮,出发 changer函数,changer 函数则调用了 setState。

const App = () => {
  const [state, setState] = useState({ color: "green" });

  const { color } = state;
  const changer = () => setState({ color: genColor() });

  return (
    <ThemeContext.Provider value={{ color, changer }}>
      <Comp />
    </ThemeContext.Provider>
  );
};

第二步:(这里不展开,setState的细节)我们 默认已经又来到了 reconciler 阶段的 App Fiber 节点,又开始调用App函数,调用 useState -> updateState, 得到了 新的颜色状态。传入 Context.Provider 组件的 props 当中。

  const [state, setState] = useState({ color: "green" });

  const { color } = state;
  const changer = () => setState({ color: genColor() });
    return (
    <ThemeContext.Provider value={{ color, changer }}>
      <Comp />
    </ThemeContext.Provider>
  );

第三步: reconciler 阶段, beginWork 函数中开始挂载 Context.Provider 组件调用updateContextProvider 函数

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {
  switch (workInProgress.tag) {
    case ContextProvider:
      return updateContextProvider(current, workInProgress, renderLanes);
  }
}

第四步:updateContextProvider 函数当中调用 pushProvider 函数。

function updateContextProvider(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
) {

  /* 这里的 workInProgress.type 就是 Provider.type = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  }; */
  const providerType: ReactProviderType<any> = workInProgress.type;
 // providerType._context; 就是 Provider.type.context
  const context: ReactContext<any> = providerType._context;
  const newProps = workInProgress.pendingProps;
  const oldProps = workInProgress.memoizedProps;
  const newValue = newProps.value;
  pushProvider(workInProgress, context, newValue);
  
  const newChildren = newProps.children;
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}

let hasWarnedAboutUsingContextAsConsumer = false;

第五步: pushProvider 函数当中将 newValue 也就是 <ThemeContext.Provider value={{ color, changer }}> 的 props 值 赋值给 context.__currentValue。现在 context.__currentValue = { color, changer }

export function pushProvider<T>(
  providerFiber: Fiber,
  context: ReactContext<T>,
  nextValue: T
): void {
  context._currentValue = nextValue;
  context._currentValue2 = nextValue;
}

第六步:默认 reconciler 阶段深度优先调度遍历到了 Comp Fiber 节点, beginWork 函数判断 workInProgress.tag根据 tag 进行挂载,函数式组件调用 renderWithHooks,在 renderWithHooks 函数当中 调用 Comp 函数。继续调用 useContext(ThemeContext)

const Comp = () => {
  const { color, changer } = useContext(ThemeContext);

  return (
    <button style={{ color, fontSize: "2em" }} onClick={changer}>
      Hello World! Click to change color
    </button>
  );
};

第七步:调用 useContext 并传入 ThemeContext,返回 context.__currentVale, 由于第四步挂载 <Context.Provider/> 的时候,已经将 context.__currntValue 修改成了 {color:"新颜色", changer: () => setState(randomColor)},所以该对象被返回。

function useContext<T>(context: ReactContext<T>): T {
  return context._currentValue;
}

第八步:Comp 返回新的虚拟 Dom,去 reconcilChildren…… reconciler 阶段结束 -> render 阶段完成之后,真实 Dom 更新,文字颜色就是上下文对象提供的更新后的 color。

return (
    <button style={{ color, fontSize: "2em" }} onClick={changer}>
      Hello World! Click to change color
    </button>
  );

自此 Context 更新阶段完成。