likes
comments
collection
share

什么时候使用 React Context ?

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

React Context 是 React 官方提供的用于深层传递 props 的能力。在官方文档中已经有了相对详细的使用说明,因此这里是想在原理层面进行一些简单的梳理。通过了解原理,帮助我们更好地考虑“是不是可以用 React Context” 来解决问题。

前置阅读

使用 Context 深层传递参数 - zh-hans.react.dev

// 示例 Context 代码
const MyContext = React.createContext(1);

const MyComponent = () => {
    return (
        <MyContext.Provider value={1}>my component</MyContext.Provider>
    );
};

Fiber 节点的树状结构

在 React 中,所熟知的虚拟 DOM 节点也被称为 Fiber 节点。例如,上面代码示例中的 <MyContext.Provider> 也算是一个 Fiber 节点。

和常见的通过 children 数组的方式来组织树状结构不同,FiberNode 依赖于 childreturnsibling 三个属性来创建树状结构。

其中:

  1. child:指向第一个子节点
  2. return:指向父节点
  3. sibling:指向下一个兄弟节点

举个例子来说:

<section>
    <div>
        <span></span>
    </div>
    <div>
        <span></span>
    </div>
    <div></div>
</section>

这段代码对应的 Fiber 节点如下:

什么时候使用 React Context ?

Fiber 树的遍历

在 React 中一般会按照类似于 深度优先遍历 的逻辑来遍历 Fiber 树。

从根节点开始,遍历规则:

  1. child 则继续访问 child
  2. sibling 则继续访问 sibling
  3. 否则访问 return

图片示例如下:

什么时候使用 React Context ?

规则1 - 寻找 child

什么时候使用 React Context ?

规则3 - return 返回

什么时候使用 React Context ?

规则2 - 寻找 sibling

需要注意的是

  1. 第一次遍历到的节点被标记成绿色,第二次遍历到的节点被标记成红色。未被遍历的节点为浅蓝色。

  2. 对于叶子节点来说,比较特殊,会被染成绿色,紧接着被染成红色。

  3. 当遍历结束后,整棵树都将被染成红色。

Provider 节点的遍历

在一棵 Fiber 树中,一个 <Provider> 组件可以多次出现,并且允许嵌套。举个例子来说:

什么时候使用 React Context ?

关于 React Context 最终的实现效果,我们知道:

  1. <Comp1> 中使用 useContext(MyContext),将获取到 顶层<MyContext.Provider> 中传入的 value
  2. <Comp2> 中相同的操作,将获取到 从上到下第3层<MyContext.Provider> 中传入的 value

这个特性便是在 Fiber 遍历 能力基础上实现。

首先,MyContext 可以看做是一个对象,类似于:

const MyContext = React.createContext(1);

// 可以理解为
const MyContext = {
    value: 1, // 1 是 createContext 时的默认值
    Provider: { // 组件,省略细节
        _context: MyContext, // 对 MyContext 对象的引用
    },
};

每个 Provider 内部都可以获取到对应的 Context,上面示例中,是以 _context 的变量形式持有 MyContext

什么时候使用 React Context ?

图例 - Provider 持有 Context 引用

当在遍历过程中,经过 <Provider> 节点时,会将传入 <Provider value={2}> 中的 value 数据存放到 MyContext.value 中:

什么时候使用 React Context ?

图例 - Provider 写入数据

关于 useContext(MyContext) ,可以简单地理解为读取 MyContext.value,即:

const value = useContext(MyContext);
// 可以简单理解为
const value = MyContext.value;

什么时候使用 React Context ?

图例 - 使用 useContext 读取数据

此时,<Comp1> 读取到的值为 value = 2

什么时候使用 React Context ?

图例 - <Comp2> 读取 value

同理,当遍历到内部的 <Provider> 组件时,会更新 MyContext.value = 3,再往下,<Comp2> 中使用 useContext() 读取的结果为 value = 3

注意

到目前为止,<Comp1><Comp2> 中都正确读取到了 MyContext 中的值,但我们仍需关于 <Comp3> 能否正确获取到值。

可能你会有一个疑问,当 <Provider>MyContext.value 中写入数据时,如果旧的数据被覆盖了,那么在某些情况下,会不会存在没有值或者说错误值的情况?

什么时候使用 React Context ?

图例 - MyContext 对应的一个数据栈

这里需要引出一个 数据栈MyContext 能够找到一个 数据栈 ,当向 MyContext.value 中写入新数据时,原本的旧数据会放存放到 数据栈 中。此时,数据栈中就存放着 value = 1value = 2 两个旧数据。

而当第二次遍历到 <Provider> 时,会对 数据栈 执行出栈动作,并将出栈的数据存放到 MyContext.value 中。此时按照遍历流程,会继续寻找 sibling,也就是 <Comp3> ,此时在 <Comp3> 中使用 useContext() 也能够拿到正确的 MyContext.value = 2

什么时候使用 React Context ?

图例 - 数据出栈 + 第一次遍历 <Comp3>

当最终遍历完成后,顶层 <Provider> 完成最终的数据出栈。

什么时候使用 React Context ?

图例 - 顶层 Provider 数据出栈

至此,我们完成了 React Context 的一部分逻辑梳理。

思考

其实在遍历流程中, <Provider> 组件 (1)所做的事情很简单,从性能角度来说,消耗相对较小。 (2)作用范围实际上很灵活,可以根据需要使用(甚至可以在循环渲染中使用) 。简单来说,就是使用成本低,同时灵活度高。

很多使用类似于 redux 的全局 store 存储的数据,如果只是局部组件使用的话,以 Context 的方式进行共享也能够达到差不多的效果,这样做可以降低全局 store 的数据存储压力。

部分需要使用 useMemo 进行缓存的数据,可以通过 Context 的方式在部分组件中共享(而不是重新再计算一次),这样可以减少渲染时的计算压力。

具体是否使用 React Context,仍需依据实际的开发场景考虑,但相信了解其部分运行原理后,现在大家用起来会更加得心应手~