React Context 是 React 官方提供的用于深层传递 props 的能力。在官方文档中已经有了相对详细的使用说明,因此这里是想在原理层面进行一些简单的梳理。通过了解原理,帮助我们更好地考虑"是不是可以用 React Context" 来解决问题。
前置阅读
typescript
// 示例 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 依赖于 child 、return 和 sibling 三个属性来创建树状结构。
其中:
child:指向第一个子节点return:指向父节点sibling:指向下一个兄弟节点
举个例子来说:
html
<section>
<div>
<span></span>
</div>
<div>
<span></span>
</div>
<div></div>
</section>
这段代码对应的 Fiber 节点如下:

Fiber 树的遍历
在 React 中一般会按照类似于 深度优先遍历 的逻辑来遍历 Fiber 树。
从根节点开始,遍历规则:
- 有
child则继续访问child - 有
sibling则继续访问sibling - 否则访问
return
图片示例如下:

规则1 - 寻找 child

规则3 - return 返回

规则2 - 寻找 sibling
需要注意的是
第一次遍历到的节点被标记成绿色,第二次遍历到的节点被标记成红色。未被遍历的节点为浅蓝色。
对于叶子节点来说,比较特殊,会被染成绿色,紧接着被染成红色。
当遍历结束后,整棵树都将被染成红色。
Provider 节点的遍历
在一棵 Fiber 树中,一个 <Provider> 组件可以多次出现,并且允许嵌套。举个例子来说:

关于 React Context 最终的实现效果,我们知道:
<Comp1>中使用useContext(MyContext),将获取到 顶层 的<MyContext.Provider>中传入的value。<Comp2>中相同的操作,将获取到 从上到下第3层 的<MyContext.Provider>中传入的value。
这个特性便是在 Fiber 遍历 能力基础上实现。
首先,MyContext 可以看做是一个对象,类似于:
typescript
const MyContext = React.createContext(1);
// 可以理解为
const MyContext = {
value: 1, // 1 是 createContext 时的默认值
Provider: { // 组件,省略细节
_context: MyContext, // 对 MyContext 对象的引用
},
};
每个 Provider 内部都可以获取到对应的 Context,上面示例中,是以 _context 的变量形式持有 MyContext 。

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

图例 - Provider 写入数据
关于 useContext(MyContext) ,可以简单地理解为读取 MyContext.value,即:
typescript
const value = useContext(MyContext);
// 可以简单理解为
const value = MyContext.value;

图例 - 使用 useContext 读取数据
此时,<Comp1> 读取到的值为 value = 2。

图例 - <Comp2> 读取 value
同理,当遍历到内部的 <Provider> 组件时,会更新 MyContext.value = 3,再往下,<Comp2> 中使用 useContext() 读取的结果为 value = 3。
注意
到目前为止,
<Comp1>和<Comp2>中都正确读取到了MyContext中的值,但我们仍需关于<Comp3>能否正确获取到值。
可能你会有一个疑问,当 <Provider> 向 MyContext.value 中写入数据时,如果旧的数据被覆盖了,那么在某些情况下,会不会存在没有值或者说错误值的情况?

图例 - MyContext 对应的一个数据栈
这里需要引出一个 数据栈 ,MyContext 能够找到一个 数据栈 ,当向 MyContext.value 中写入新数据时,原本的旧数据会放存放到 数据栈 中。此时,数据栈中就存放着 value = 1 和 value = 2 两个旧数据。
而当第二次遍历到 <Provider> 时,会对 数据栈 执行出栈动作,并将出栈的数据存放到 MyContext.value 中。此时按照遍历流程,会继续寻找 sibling,也就是 <Comp3> ,此时在 <Comp3> 中使用 useContext() 也能够拿到正确的 MyContext.value = 2。

图例 - 数据出栈 + 第一次遍历 <Comp3>
当最终遍历完成后,顶层 <Provider> 完成最终的数据出栈。

图例 - 顶层 Provider 数据出栈
至此,我们完成了 React Context 的一部分逻辑梳理。
思考
其实在遍历流程中, <Provider> 组件 (1)所做的事情很简单,从性能角度来说,消耗相对较小。 (2)作用范围实际上很灵活,可以根据需要使用(甚至可以在循环渲染中使用) 。简单来说,就是使用成本低,同时灵活度高。
很多使用类似于 redux 的全局 store 存储的数据,如果只是局部组件使用的话,以 Context 的方式进行共享也能够达到差不多的效果,这样做可以降低全局 store 的数据存储压力。
部分需要使用 useMemo 进行缓存的数据,可以通过 Context 的方式在部分组件中共享(而不是重新再计算一次),这样可以减少渲染时的计算压力。
具体是否使用 React Context,仍需依据实际的开发场景考虑,但相信了解其部分运行原理后,现在大家用起来会更加得心应手~