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,仍需依据实际的开发场景考虑,但相信了解其部分运行原理后,现在大家用起来会更加得心应手~