【React Hooks原理 - createContext、useContext】

概述

在前面React Hooks系列介绍中我们知道React为了更好的处理不同生命周期的特殊处理,除了useContext这个Hooks之外的其他Hooks都拆为了Mount、Update两个阶段,而useContext内部并没有区分都是通过调用readContext来获取上下文的,下面就来介绍一下useContext为什么不拆分以及其内部实现原理?

基本概念

先回答上下面的问题: 为什么useContext不和其他Hooks一样拆分为Mount、Update阶段处理呢?

  • 这是因为React之所以拆分是针对复杂的状态和副作用管理这样可以更新清晰的处理不同生命周期的处理,比如Mount主要关注初始化挂载,Update主要进行更新渲染,而useContext只是订阅并获取上下文的值,不存在复杂的状态和副作用,所以不需要拆分过程。

useContext可以帮助我们跨越组件层级直接传递变量,避免了在每一个层级手动的传递 props 属性,进而实现状态共享。它的工作就是订阅上下文,并当该上下文变化时,触发组件更新重新渲染,所以这个Hook依赖createContext这个API来先创建一个上下文。本文也会从createContext入手先介绍createContext自带的Provider、Consumer然后介绍useContext。

基本使用

createContext

我们知道createContext接收一个默认值,会返回一个包含Provider、Consumer的上下面,其中Provider作为上下文数据的提供者,Consumer作为数据的消费者,以一个接收上下文数据并返回一段JSX的函数进行消费。

PS:Consumer只能获取上级Provider提供的数据,同级或者子级的无法获取。useContext和Consumer使用的是同一个算法去查找上下文,所以也是无法获取同级/子级别数据

createContext接收的默认值永远不会变化,只是一个异常的兜底策略,当通过Comsumer消费时会从上级组件逐渐往上查找,直到找到最近的Provider提供者,并获取其提供的值,如果到root根节点都没有找到则会使用该默认值。

Demo实例:

javascript 复制代码
// App.js
const SomeContext = createContext(defaultValue)
function App() {
  const [theme, setTheme] = useState('light');
  // ......
  return (
    <ThemeContext.Provider value={theme}>
      <Page />
    </ThemeContext.Provider>
  );
}

// Page.js
function Page() {
  // 遗留方式 (不推荐)
  return (
    <ThemeContext.Consumer>
      {theme => (
        <button className={theme} />
      )}
    </ThemeContext.Consumer>
  );
}

正如React官网所说目前不推荐使用Consumer获取上下文的方式,但是useContext是一个Hook只能在函数式组件或者自定义Hook中使用,所以在类组件中常使用该方式获取。虽然目前主流使用函数式组件,但是一些较老的项目中仍然使用类组件方式,所以这里简单介绍下。

useContext

useContext是React推荐的在函数式组件中获取上下文信息的一个Hook,其只能在函数组件顶层以及自定义Hook中使用。该Hook简化了获取上下文的过程,并解决了使用Consumer函数式返回的使用成本,将数据直接作为对象返回,使代码更简洁和清晰。

javascript 复制代码
// Page.js
function Page() {
  // ✅ 推荐方式
  const theme = useContext(ThemeContext);
  return <button className={theme} />;
}

源码解析

经过上面介绍,对其使用已经有了初步了解,这里就直接上代码。

createContext

createContext定义:

javascript 复制代码
export function createContext<T>(defaultValue: T): ReactContext<T> {
  const context: ReactContext<T> = {
  // flag标识是一个symbol常量
  // export const REACT_CONTEXT_TYPE: symbol = Symbol.for('react.context');
    $$typeof: REACT_CONTEXT_TYPE, 
    _currentValue: defaultValue, // 用于主渲染器 React-dom(web端)、react-native(移动端)
    _currentValue2: defaultValue, // 用于次渲染器 React ART(web端)和React Fabric(移动端)。
    _threadCount: 0, // _threadCount 用于追踪当前有多少个并发任务在使用该上下文,便于更好的状态管理
    // These are circular
    Provider: (null: any), // 上下文数据提供 value = any
    Consumer: (null: any), // 获取上下文信息 children = (value) = ReactElement
  };
  // enableRenderableContext用于控制上下文对象的 Provider 和 Consumer 的设置方式。一般为true,所以这里截取true的代码
  (context.Provider = context;
    context.Consumer = {
      $$typeof: REACT_CONSUMER_TYPE,
      _context: context,
    };
  return context;
}

从代码能看出,createContext接收一个默认值defaultValue,然后会返回一个上下文context,其本质就是一个带有特殊属性的对象,其中会设置context.Provider指向context本身,而Consumer指向带有$$typeof和context的对象。在这里虽然Provider指向context本身,其$$typeof是REACT_CONTEXT_TYPE,但是React内部也会按照REACT_PROVIDER_TYPE来处理Provider

这里需要注意$$typeof属性,因为这个字段让React知道当前对象是什么,比如这里context是一个对象,但是我们可以通过组件的方式使用<Context.Provider />,就是因为React根据$$typeof判断将Provider、Consumer渲染为了一个组件fiber节点。

在协调阶段React会根据JSX代码将其转换为Vdom然后转换为fiber节点并进行对比,最后生成需要更新的fiber树WorkInProgress Tree,然后通知渲染器渲染。流程可以理解为: JSX -> Vdom Tree -> Fiber Tree -> WorkInProgress Tree -> Renderer

Provider

从上面知道在使用createContext时返回带有$$typeof的context对象后,我们可以在组件内通过<Context.Provide value={state}>组件的方式来使用Provider,现在来看看其内部是如何实现的。

我们知道所有节点会在协调阶段会进行fiber构造循环通过createFiberFromElement函数将虚拟dom转换为fiber节点进而生成fiber树,在fiber构造时会进入到beginWork进行fiber节点的处理,在该函数中会对Provider类型的节点进行处理:

在执行createFiberFromElement函数时会调用createFiberFromTypeAndProps其中会将Provider打上ContextProvider的fiberTag

javascript 复制代码
if (typeof type === 'object' && type !== null) {
switch (type.$$typeof) {
  case REACT_PROVIDER_TYPE:
    if (!enableRenderableContext) {
      fiberTag = ContextProvider;
      break getTag;
    }
  // Fall through
  case REACT_CONTEXT_TYPE:
    if (enableRenderableContext) {
      fiberTag = ContextProvider;
      break getTag;
    } else {
      fiberTag = ContextConsumer;
      break getTag;
    }
  case REACT_CONSUMER_TYPE:
    if (enableRenderableContext) {
      fiberTag = ContextConsumer;
      break getTag;
    }

由此可见React会将REACT_PROVIDER_TYPEREACT_CONTEXT_TYPE类型都处理为ContextProvider组件,这就解释了上面createContext时Provider的$$typeof为什么为REACT_CONTEXT_TYPE却能使用Provider组件的原因。

javascript 复制代码
function beginWork(
    current: Fiber | null,
    workInProgress: Fiber,
    renderLanes: Lanes
  ): Fiber | null {
    switch (workInProgress.tag) {
        case ContextProvider:
        return updateContextProvider(current, workInProgress, renderLanes);
        case ContextConsumer:
        return updateContextConsumer(current, workInProgress, renderLanes);
        default: ;
}

在处理ContextProvider的tag时会调用updateContextProvider函数进行以下处理:

  • 通过pushProvider将Provider提供的新的上下文保存在context.current中

  • 通过Object.is判断上下文是否变化

    • 如果没有上次遗留的变化以及子节点改变则调用bailoutOnAlreadyFinishedWork跳过本次更新
    • 如果上下文变化则调用propagateContextChange通过所以订阅该上下文的子级更新
javascript 复制代码
function updateContextProvider(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
) {
  const newProps = workInProgress.pendingProps;
  const oldProps = workInProgress.memoizedProps;

  const newValue = newProps.value;

  // 将newValue保存在context.current中
  pushProvider(workInProgress, context, newValue);

  if (oldProps !== null) {
    const oldValue = oldProps.value;
    if (is(oldValue, newValue)) {
      if (
        // 检查 children 是否相同以及是否有遗留上下文变化。
        oldProps.children === newProps.children &&
        !hasLegacyContextChanged()
      ) {
        // 跳过本次更新
        return bailoutOnAlreadyFinishedWork(
          current,
          workInProgress,
          renderLanes
        );
      }
    } else {
      // 上下文改变,该函数会找到所有使用了这个上下文的消费者,并标记这些消费者需要更新。
      propagateContextChange(workInProgress, context, renderLanes);
    }
  }
}

function pushProvider<T>(
    providerFiber: Fiber,
    context: ReactContext<T>,
    nextValue: T,
  ): void {
    // 将context._currentValue 保存在valueCursor.current上
    push(valueCursor, context._currentValue, providerFiber);
    // 更新context._currentValue为最新的上下文
    context._currentValue = nextValue;
}

// 保存上下文的栈,先保存旧的上下文等pop的时候可以恢复,然后更新context.current为最新的上下文
function push<T>(cursor: StackCursor<T>, value: T, fiber: Fiber): void {
    index++;
  
    valueStack[index] = cursor.current;  
    cursor.current = value;
}

propagateContextChange函数主要就是调用propagateContextChange_eager使用类似DFS算法来查找所有订阅该上下文的组件并通知其进行更新。

javascript 复制代码
// 使用DFS深度优先遍历查找所有消费上下文的组件并添加需要更新的标记,类组件会创建强制刷新forceUpdate并添加到更新队列中
function propagateContextChange_eager<T>(
  workInProgress: Fiber,
  context: ReactContext<T>,
  renderLanes: Lanes
): void {
  // DFS遍历当前节点
  while (fiber !== null) {
    let nextFiber;

    // 使用useContext/Consumer消费时在首次挂载时候就会创建一个依赖列表并将其所依赖的上下文保存在fiber.dependency中
    const list = fiber.dependencies;
    if (list !== null) {
      nextFiber = fiber.child;

      let dependency = list.firstContext;
      while (dependency !== null) {
        // 判断上下文是否变化
        if (dependency.context === context) {
          // 对类组件更新更新
          if (fiber.tag === ClassComponent) {
            // 创建一个强制更新并添加到更新队列中
            const lane = pickArbitraryLane(renderLanes);
            const update = createUpdate(lane);
            update.tag = ForceUpdate;
            const updateQueue = fiber.updateQueue;
            if (updateQueue === null) {
              // Only occurs if the fiber has been unmounted.
            } else {
              const sharedQueue: SharedQueue<any> = (updateQueue: any).shared;
              const pending = sharedQueue.pending;
              if (pending === null) {
                // 首次挂载,创建一个循环链表
                update.next = update;
              } else {
                update.next = pending.next;
                pending.next = update;
              }
              sharedQueue.pending = update;
            }
          }
          // 向上遍历: 从consumer节点开始, 向上遍历, 修改父路径上所有节点的fiber.childLanes属性, 表明其子节点有改动, 子节点会进入更新逻辑.
          scheduleContextWorkOnParentPath(
            fiber.return,
            renderLanes,
            workInProgress,
          );
          ...
          break;
        }
        dependency = dependency.next;
      }
    } else if (fiber.tag === ContextProvider) {
      // 性能优化,跳过Provider提供者,避免不必要的查找
      nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
    } else {
      // 往下遍历
      nextFiber = fiber.child;
    }

    // 根据nextFiber指针,继续往下遍历
    ...
  }
}

从代码和注释我们知道,主要是调用propagateContextChange_eager函数来查找和通知子组件更新:

  • 通过workInProgress使用DFS深度优先算法遍历该组件下的所有子组件
  • 通过dependency.context === context知道当前fiber节点订阅了该上下文
    • 通过fiber.tag === ClassComponent处理类组件,会创建一个强制刷新任务ForceUpdate并添加到更新队列updateQueue中
    • 然后调用scheduleContextWorkOnParentPath向上遍历父路径上所有节点的fiber.childLanes属性, 表明其子节点有改动, 子节点会进入更新逻辑

scheduleContextWorkOnParentPath函数如下:主要就是按照父路径以此向上遍历,并更新node.childLanes属性,让React知道哪些节点需要更新,在后续协调阶段会统一将依赖该上下文的节点更新。

javascript 复制代码
// 修改父路径上所有节点的fiber.childLanes属性, 表明其子节点有改动, 子节点会进入更新逻辑
export function scheduleContextWorkOnParentPath(
  parent: Fiber | null,
  renderLanes: Lanes,
  propagationRoot: Fiber,
) {
  // Update the child lanes of all the ancestors, including the alternates.
  let node = parent;
  while (node !== null) {
    const alternate = node.alternate;
    if (!isSubsetOfLanes(node.childLanes, renderLanes)) {
      node.childLanes = mergeLanes(node.childLanes, renderLanes);
      if (alternate !== null) {
        alternate.childLanes = mergeLanes(alternate.childLanes, renderLanes);
      }
    } else if (
      alternate !== null &&
      !isSubsetOfLanes(alternate.childLanes, renderLanes)
    ) {
      alternate.childLanes = mergeLanes(alternate.childLanes, renderLanes);
    } else {
      // Neither alternate was updated.
      // Normally, this would mean that the rest of the
      // ancestor path already has sufficient priority.
      // However, this is not necessarily true inside offscreen
      // or fallback trees because childLanes may be inconsistent
      // with the surroundings. This is why we continue the loop.
    }
    if (node === propagationRoot) {
      break;
    }
    node = node.return;
  }
}

这里面涉及到isSubsetOfLanesmergeLanes两个函数,这两个函数主要是对优先级进行的检查和合并操作。因为一个节点可能有多个操作,所以需要有多个优先级标记,React内部使用二进制管理,当有多个操作时,会合并优先级。

javascript 复制代码
export function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane): boolean {
  return (set & subset) === subset;
}

export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
  return a | b;
}

看代码可知isSubsetOfLanes函数主要是按位与操作 (&) 来检查一个 lane 是否是另一个 lane 的子集。它返回一个布尔值,表示 subset 中的所有位是否都在 set 中设置。

mergeLanes函数使用按位或操作 (|) 将两个 lanes 合并在一起。这意味着,如果一个节点有多个操作,这些操作的优先级会被合并到一个值中。

举个例子

假设你有一个组件树,根节点是 A,其子节点是 B 和 C,其中 C 的子节点是 D 和 E。如果 D 的上下文发生变化,需要更新,那么 scheduleContextWorkOnParentPath 会执行以下操作:

  • 检查节点 C:

    • isSubsetOfLanes(renderLanes, C.childLanes) 返回 false,因为 C 的 childLanes 可能还不包含 D 的更新优先级。
    • 合并 D 的更新优先级到 C 的 childLanes,即 C.childLanes = mergeLanes(C.childLanes, renderLanes)。
  • 检查节点 A:

    • 同样的逻辑,isSubsetOfLanes(renderLanes, A.childLanes) 返回 false。
    • 合并更新优先级到 A 的 childLanes,即 A.childLanes = mergeLanes(A.childLanes, renderLanes)。

isSubsetOfLanes 确保只有当需要更新时才会进行合并操作,而 mergeLanes 确保所有需要的更新优先级都被正确地标记在 childLanes 上。在后续的更新过程中,React 会使用这些标记来高效地确定哪些节点需要更新,从而实现高效的更新机制。

至此整个创建上下文以及上下文变化之后Provider会通知所有订阅该上下文的子组件进行更新的流程我们就梳理完了。在这里小结一下:使用createContext创建上下文后,会返回包含Provider、Consumer属性的上下文对象,React会根据其内部的$$typeof属性将该对象识别为ContextProvider/ContextConsumer类型的组件,当通过Provider的value属性修改上下文时,会在beginWork函数中调用updateContextProvider将newValue绑定到context.current上,并且会调用propagateContextChange_eager函数通过DFS算法来查找所有依赖该上下文的组件并根据父路径向上遍历通过mergeLanes更改parent.childLanes属性来表示其子组件需要更新,最后在协调阶段的fiber构造时对比更新

现在我们知道了创建和更新上下文的原理,下面我们来介绍下在组件中我们如何消费的原理。

一般我们在类组件通过Context.Consumer来消费上下文,在函数组件中通过useContext这个Hook来消费,所以下面从这两个API来入手介绍,其中这两个API都是使用相同的算法readContext来获取上下文的,所以下面先从useContext入手

useContext

先看useContext定义:

javascript 复制代码
export function useContext<T>(Context: ReactContext<T>): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useContext(Context);
}

同其他Hooks一样,使用执行的函数会有dispatcher进行派发,但是context比较特殊,在Mount、Update都是执行的reactContext函数,下面看看该函数干了什么?

javascript 复制代码
export function readContext<T>(context: ReactContext<T>): T {
  return readContextForConsumer(currentlyRenderingFiber, context);
}

readContext函数只是调用了readContextForConsumer,在其内部主要是创建了依赖列表(链表形式管理),以便当更新上下文时能找到依赖该上下文的组件。即上面propagateContextChange_eager函数中的dependency.context === context

javascript 复制代码
function readContextForConsumer<T>(
  consumer: Fiber | null,
  context: ReactContext<T>
): T {
 // isPrimaryRenderer是否是主渲染,获取当前上下文的值
  const value = isPrimaryRenderer
    ? context._currentValue
    : context._currentValue2;

  if (lastFullyObservedContext === context) {
    // 表示当前fiber依赖的上下文没有变化,不需要处理
  } else {
    const contextItem = {
      context: ((context: any): ReactContext<mixed>),
      memoizedValue: value,
      next: null, // 因为一个组件即fiber可以创建/依赖多个上下文,所以这里使用链表管理
    };

    if (lastContextDependency === null) {
      // This is the first dependency for this component. Create a new list.
      lastContextDependency = contextItem;
      consumer.dependencies = {
        lanes: NoLanes,
        firstContext: contextItem,
      };
    } else {
      // 以及有依赖上下文列表则直接添加
      lastContextDependency = lastContextDependency.next = contextItem;
    }
  }
  return value;
}

从代码中可知会根据当前上下文创建一个依赖快照contextItem并保存在依赖列表中fiber.dependencies,如果是第一个依赖则需要创建一个依赖列表,否则直接将当前创建的快照添加到依赖列表中即可。然后返回当前的上下文value

由此就能看出,useContext这个Hook接收一个上下文context然后返回上下文的值对象context._currentValue,即const value = useContext(context)。当上下文变化之后,Provider会在pushProvider函数中将context._currentValue修改为新的value值,然后通知依赖该上下文的组件更新,在该组件中通过context._currentValue获取新的value值。这里的上下文不会改变,变化的只是上下文中的value值,即_currentValue属性

Consumer

这个API主要在类组件中使用,是有createContext创建上下文之后自带的属性,其必须包裹JSX代码组件才能访问上下文信息,在上面的beginWork中得知,当使用Consumer时是执行的updateContextConsumer函数,简略代码如下:

javascript 复制代码
function updateContextConsumer(
    current: Fiber | null,
    workInProgress: Fiber,
    renderLanes: Lanes,
  ) {
    let context: ReactContext<any>;
    const consumerType: ReactConsumerType<any> = workInProgress.type;
    context = consumerType._context;
    const newProps = workInProgress.pendingProps;
    // 传入的render函数
    const render = newProps.children;

    const newValue = readContext(context);
    let newChildren;
    newChildren = render(newValue);
  
    // React DevTools reads this flag.
    workInProgress.flags |= PerformedWork;
    reconcileChildren(current, workInProgress, newChildren, renderLanes);
    return workInProgress.child;
}

从上面代码能看出来Consumer内部也是调用的readContext来获取上下文value的,然后将该value传入的render,这就是为什么使用Consumer消费上下文时,必须要使用Consumer包裹一个函数来使用上下文的原因。

总结

当使用上下文时,需要通过createContext来创建一个上下文,会返回一个包含$$typeof、Provider、Consumer属性的对象,React会根据$$typeofProvider、Consumer类型处理为组件,所以我们可以通过<Context.Provider><Context.Consumer>来使用,其中Provider作为上下文的提供方用于提供/更新共享的value,当上下文变化之后,Provider会在pushProvider函数中将context._currentValue修改为新的value值,然后利用DFS通知依赖该上下文的所有组件更新,在更新组件中执行readContext函数(Consumer和useContext都是使用的readContext获取上下文值)通过context._currentValue获取新的value值然后进行更新渲染。这里的上下文不会改变,变化的只是上下文中的value值,即_currentValue属性

具体详细流程如下:

  • 创建上下文:

    • 使用 createContext 创建包含 Provider 和 Consumer 的上下文对象。
  • 提供上下文值:

    • 在组件树中使用 Provider 组件,传递 value 给子组件。
  • 消费上下文值:

    • 使用 Consumer 组件或 useContext Hook 获取上下文值。
  • 上下文值变化时的处理:

    • 在 Provider 中,新的 value 传入后,会比较新旧值。
    • 如果值发生变化,会调用 propagateContextChange -> propagateContextChange_eager 函数。
    • propagateContextChange_eager 函数遍历组件树,找到依赖该上下文的组件,并标记这些组件进行更新。
    • 使用 scheduleContextWorkOnParentPath 函数更新父路径上的 childLanes 属性,以确保在后续渲染中处理更新。
  • 更新组件:

    • 在更新阶段,React 会检查每个 Fiber 节点的 lanes 和 childLanes 属性,确定需要更新的组件,并重新渲染这些组件。
相关推荐
Json____11 分钟前
学法减分交管12123模拟练习小程序源码前端和后端和搭建教程
前端·后端·学习·小程序·uni-app·学法减分·驾考题库
迂 幵19 分钟前
vue el-table 超出隐藏移入弹窗显示
javascript·vue.js·elementui
上趣工作室23 分钟前
vue2在el-dialog打开的时候使该el-dialog中的某个输入框获得焦点方法总结
前端·javascript·vue.js
家里有只小肥猫24 分钟前
el-tree 父节点隐藏
前端·javascript·vue.js
fkalis25 分钟前
【海外SRC漏洞挖掘】谷歌语法发现XSS+Waf Bypass
前端·xss
zxg_神说要有光1 小时前
自由职业第二年,我忘记了为什么出发
前端·javascript·程序员
陈随易1 小时前
农村程序员-关于小孩教育的思考
前端·后端·程序员
云深时现月1 小时前
jenkins使用cli发行uni-app到h5
前端·uni-app·jenkins
昨天今天明天好多天2 小时前
【Node.js]
前端·node.js
亿牛云爬虫专家2 小时前
Puppeteer教程:使用CSS选择器点击和爬取动态数据
javascript·css·爬虫·爬虫代理·puppeteer·代理ip