深入理解React Context,助你解决项目开发中的百分之90状态管理💯💯💯

在 React 中,Context 是一种用于在组件之间共享数据的方法,而无需通过逐层传递 props 来传递数据。它可以被看作是 React 组件树中的一个全局状态容器,使得某些数据能够被跨越组件层级传递,从而简化了组件之间数据传递的复杂性。

Context 通常由两个主要部分组成:Provider 和 Consumer:

  • Provider:Provider 组件负责将数据提供给子组件。它包裹在需要访问这些共享数据的组件外部,并通过 value 属性将数据传递给子组件。

  • Consumer:Consumer 组件允许子组件订阅 Context 中的数据。通过在组件内部渲染一个函数,您可以访问 Provider 提供的数据。

了解了关于 Context 的基本信息之后,废话少说,直接上号。

创建 Context

创建一个 Context,需要通过 React.createContext 这个 api 来创建 context 对象,在 React 源码中有如下代码所示:

在这个函数中首先创建一个 context 对象,并将初始值传递给 _currentValue_currentValue2,保存两个值是为了支持多个渲染器并发渲染,例如 React Native 和 React DOM 可能在同一个应用中并行工作。

_threadCount:用于跟踪在单个渲染器内当前支持多少个并发渲染器,例如用于并行服务器渲染。

例如,在项目中我们创建了如下 Context:

jsx 复制代码
import { createContext } from "react";

const Context = createContext("岂曰无衣");

export default Context;

这时候,我们通过 debugger 可以查看出有如下结构所示:

Provider 提供者

Context 中的 Provider 本质上是一个 React 组件,它的作用是提供一个特定的数据源,以便被子组件订阅并访问。Provider 组件在 React 的组件树中包裹着那些需要访问共享数据的子组件。

所以也就有以下的对比关系:

rust 复制代码
jsx -> element -> fiber

<Provider> -> REACT_PROVIDER_TYPE React element -> ContextProvider fiber

在 Fiber 树渲染阶段,也就是调和阶段,会调用 beginWork 函数:

ts 复制代码
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {
  // 清空过期时间
  workInProgress.lanes = NoLanes;
  switch (workInProgress.tag) {
    case ContextProvider:
      return updateContextProvider(current, workInProgress, renderLanes);
    case ContextConsumer:
      return updateContextConsumer(current, workInProgress, renderLanes);
  }
}

如果 workInProgress.tag 是 ContextProvider 类型的话会调用 updateContextProvider 函数进行相应的处理。

首先我们来看看此时 fiber 树的结构:

workInProgress 是正在进行中的工作单元,表示正在进行更新的组件实例。而 workInProgress.pendingProps 则是这个正在进行的工作单元所持有的最新的 props,也就是即将应用到组件上的 props。

这也证实了我们前面中所说到的 Provider 本质上是一个 React 组件。而它的 children 也就是被 Provider 包裹的组件:

jsx 复制代码
const App = () => {
  return (
    <Context.Provider value={"与子同袍"}>
      <Moment />
    </Context.Provider>
  );
};

这就完美对应上了。

在 beginWork 函数中,如果当前类型的 fiber 不需要更新,那么会 FinishedWork 中止当前节点和子节点的更新。

如果当前类型 fiber 需要更新,那么会调用不同类型 fiber 的处理方法。 updateContextProvider 方法就是用来更新 Context 的。

updateContextProvider

该函数的主要功能如下代码所示:

ts 复制代码
function updateContextProvider(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
) {
  const newProps = workInProgress.pendingProps;
  const oldProps = workInProgress.memoizedProps;

  const newValue = newProps.value;

  pushProvider(workInProgress, context, newValue);

  if (enableLazyContextPropagation) {
  } else {
    if (oldProps !== null) {
      const oldValue = oldProps.value;
      if (is(oldValue, newValue)) {
        // No change. Bailout early if children are the same.
        if (
          oldProps.children === newProps.children &&
          !hasLegacyContextChanged()
        ) {
          return bailoutOnAlreadyFinishedWork(
            current,
            workInProgress,
            renderLanes
          );
        }
      } else {
        propagateContextChange(workInProgress, context, renderLanes);
      }
    }
  }

  const newChildren = newProps.children;
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  return workInProgress.child;
}

在该函数中首先会调用 pushProvider 函数将最新的值赋值给 context._currentValuecontext._currentValue2

接下来检查旧的上下文值是否与新值相同。如果相同,则检查子树是否相同,如果相同,则直接跳过这个子树的更新并使用 bailoutOnAlreadyFinishedWork 进行优化。

如果上下文值不同,表示上下文发生了变化,会调用 propagateContextChange 函数来通知所有依赖此 context 的 fiber,并标记相关 Consumer 需要进行更新。

通过 reconcileChildren 函数,对 Provider 的子元素进行调和,处理子元素的更新。

返回 workInProgress.child,即 Provider 的第一个子元素。

在浏览器终端有这样的对象结构,如下图所示:

其中 "岂曰无衣" 是刚开始创建时传递的初始值,而 "与子同袍" 是我们在调用 Provider 传递的值。

propagateContextChange

接下里我们详细讲讲这个函数,它为子组件中,所有依赖此 Context 的组件安排一个更新,接下来进入到该函数中,由于核心逻辑在 propagateContextChange_eager 中,直接看 propagateContextChange_eager 代码吧:

ts 复制代码
function propagateContextChange_eager<T>(
  workInProgress: Fiber,
  context: ReactContext<T>,
  renderLanes: Lanes
): void {
  let fiber = workInProgress.child;
  if (fiber !== null) {
    fiber.return = workInProgress;
  }
  // 从子节点开始匹配是否存在消费了当前 Context 的节点
  while (fiber !== null) {
    let nextFiber;

    const list = fiber.dependencies;
    if (list !== null) {
      nextFiber = fiber.child;

      let dependency = list.firstContext;
      while (dependency !== null) {
        // 1. 判断 fiber 节点的 context 和当前 context 是否匹配
        if (dependency.context === context) {
          // 2. 匹配时,给当前节点调度一个更新任务
          if (fiber.tag === ClassComponent) {
          }

          fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
          const alternate = fiber.alternate;
          if (alternate !== null) {
            alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
          }
          // 3. 向上标记 childLanes
          scheduleContextWorkOnParentPath(
            fiber.return,
            renderLanes,
            workInProgress
          );

          list.lanes = mergeLanes(list.lanes, renderLanes);
          break;
        }
        dependency = dependency.next;
      }
    } else if (fiber.tag === ContextProvider) {
    } else if (fiber.tag === DehydratedFragment) {
    } else {
    }
    fiber = nextFiber;
  }
}

该函数的主要功能有以下两个方面:

  • 从 ContextProvider 的节点出发,向下查找所有 fiber.dependencies 依赖当前 Context 的节点。
  • 找到消费节点时,从当前节点出发,向上回溯标记父节点 fiber.childLanes,标识其子节点需要更新,从而保证了所有消费了该 Context 的子节点都会被重新渲染,实现了 Context 的更新

Context 会强制安排更新,无视 PureComponent、Memo、shouldComponentUpdate 等优化逻辑。

如上面图中代码所示,当我们 Context 发生了变化,就算 memo 包裹的组件也会重新渲染。

当子组件的上层多次使用了同一个 Context 的 Provider 时,子组件会获取离自己最近的那个 Provider 所提供的上下文值。也就是说,React 会沿着组件树自上而下的顺序,查找最近的 Provider,并使用它提供的值。

到这里的时候,Provider 已经基本讲解结束,但是 Consumer 已经基本不适用了,现在都是在用 useContext 了。

useContext

在前面的例子中我们已经知道了 useContext 需要将 createContext 创建的 Context 作为参数进行调用。

mount 阶段

useContext 在 mount 时主要会调用 readContext 函数:

那么我们来看看 readContext 函数是在如何工作的,如下代码所示:

ts 复制代码
export function readContext<T>(context: ReactContext<T>): T {
  const value = isPrimaryRenderer
    ? context._currentValue
    : context._currentValue2;

  if (lastFullyObservedContext === context) {
  } else {
    const contextItem = {
      context: ((context: any): ReactContext<mixed>),
      memoizedValue: value,
      next: null,
    };
    if (lastContextDependency === null) {

      lastContextDependency = contextItem;
      currentlyRenderingFiber.dependencies = {
        lanes: NoLanes,
        firstContext: contextItem,
      };
      if (enableLazyContextPropagation) {
        currentlyRenderingFiber.flags |= NeedsPropagation;
      }
    } else {
      // Append a new context item.
      lastContextDependency = lastContextDependency.next = contextItem;
    }
  }
  return value;
}

在该函数中,主要有以下几个核心逻辑:

  1. 创建一个 contextItem 对象。 该对象的数据如下图所示:

    其中 next 是指向下一个 context 项

  2. fiber 上会存在多个 dependencies,它们以链表的形式联系到一起,如果不存在最后一个 context dependency,context dependencies 为空 ,那么会创建第一个 dependency。

  3. 如果存在最后一个 dependency,那么 contextItem 会以链表形式保存,并变成最后一个 lastContextDependency。 这就是我们前面中说到的 React 会沿着组件树自上而下的顺序,查找最近的 Provider,并使用它提供的值。

  4. 最终返回 value,也就是 Provider 提供的值:

返回该值之后继续执行 Fiber 树构造直到全部完成之后。

没了,挺简单的。

原理总结

通过 React.createContext() 创建一个 Context 对象。这个对象包含 Provider 和 Consumer 组件以及一些其他属性。

使用 <Context.Provider> 组件将需要共享的数据包裹在内,通过 value 属性传递数据。

使用 <Context.Consumer> 组件或 useContext Hook 在需要的组件中访问共享数据。这两种方式都会在组件树中向上查找最近的匹配的 Provider,也就是 Fiber 树上 dependencies,并提取其 value 属性传递给组件。

当 Provider 提供的数据发生变化时,会触发依赖于这些数据的组件的重新渲染,无论你是使用 PureComponent、Memo、shouldComponentUpdate 等优化逻辑。

我是如何爱上了 Context 的

我最近在使用 webContainer 开发一个在线代码编辑器,它就类似一个虚拟机,要在页面初始化时对其开机并保存其实例,后面都要用到,一开始我用的是 redux,接下来我们来看看 Redux 会给我们带来什么样的效果:

直接报错,原因是 redux 在存储之前会对数据进行序列化,将 JavaScript 对象转换成字符串的过程,以便在不同的环境中传输和存储数据,也就是说实例的方法都没了,那还玩个毛线。

于是我便使用了 Context 的方式去掉了 redux:

并将 webContainerInstance 通过 Provider 传递给其他组件使用:

从而可以使多个组件同时使用一个实例。

总结

巧用 Context,能解决你在项目中百分之 90 的场景。

项目地址:github.com/xun082/onli...

相关推荐
小曲曲1 小时前
接口上传视频和oss直传视频到阿里云组件
javascript·阿里云·音视频
学不会•2 小时前
css数据不固定情况下,循环加不同背景颜色
前端·javascript·html
EasyNTS3 小时前
H.264/H.265播放器EasyPlayer.js视频流媒体播放器关于websocket1006的异常断连
javascript·h.265·h.264
活宝小娜4 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点4 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow4 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o4 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
开心工作室_kaic5 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
刚刚好ā5 小时前
js作用域超全介绍--全局作用域、局部作用、块级作用域
前端·javascript·vue.js·vue
沉默璇年7 小时前
react中useMemo的使用场景
前端·react.js·前端框架