很多项目的源码非常复杂,让人望而却步。但在打退堂鼓前,我们应该思考一个问题:源码为什么复杂?
造成源码复杂的原因不外乎有三个:
- 功能本身复杂,造成代码复杂
- 编写者功力不行,写的代码复杂
- 功能本身不复杂,但同一个模块耦合了太多功能,看起来复杂
如果是原因3,那实际理解起来其实并不难。我们需要的只是有人能帮我们剔除无关功能的干扰。
React Context
的实现就是个典型例子,当剔除无关功能的干扰后,他的核心实现,仅需「5行代码」。
本文就让我们看看React Context
的核心实现。
简化模型
Context
的完整工作流程包括3步:
- 定义
context
- 赋值
context
- 消费
context
以下面的代码举例:
代码语言:javascript复制const ctx = createContext(null);
function App() {
return (
<ctx.Provider value={1}>
<Cpn />
</ctx.Provider>
);
}
function Cpn() {
const num = useContext(ctx);
return <div>{num}</div>;
}
其中:
const ctx = createContext(null)
用于定义<ctx.Provider value={1}>
用于赋值const num = useContext(ctx)
用于消费
Context
数据结构(即createContext
方法的返回值)也很简单:
function createContext(defaultValue) {
const context = {
$$typeof: REACT_CONTEXT_TYPE,
Provider: null,
_currentValue: defaultValue
};
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context
};
return context;
}
其中context._currentValue
保存context
当前值。
context
工作流程的三个步骤其实可以概括为:
- 实例化
context
,并将默认值defaultValue
赋值给context._currentValue
- 每遇到一个同类型
context.Provier
,将value
赋值给context._currentValue
useContext(context)
就是简单的取context._currentValue
的值就行
了解了工作流程后我们会发现,Context
的核心实现其实就是步骤2。
核心实现
核心实现需要考虑什么呢?还是以上面的示例为例,当前只有一层<ctx.Provider>
包裹<Cpn />
:
function App() {
return (
<ctx.Provider value={1}>
<Cpn />
</ctx.Provider>
);
}
在实际项目中,消费ctx
的组件(示例中的<Cpn/>
)可能被多级<ctx.Provider>
包裹,比如:
const ctx = createContext(0);
function App() {
return (
<ctx.Provider value={1}>
<ctx.Provider value={2}>
<ctx.Provider value={3}>
<Cpn />
</ctx.Provider>
<Cpn />
</ctx.Provider>
<Cpn />
</ctx.Provider>
);
}
在上面代码中,ctx
的值会从0(默认值)逐级变为3,再从3逐级变为0,所以沿途消费ctx
的<Cpn />
组件取得的值分别为:3、2、1。
整个流程就像「操作一个栈」,1、2、3分别入栈,3、2、1分别出栈,过程中栈顶的值就是context
当前的值。
基于此,context
的核心逻辑包括两个函数:
function pushProvider(context, newValue) {
// ...
}
function popProvider(context) {
// ...
}
其中:
- 进入
ctx.Provider
时,执行pushProvider
方法,类比入栈操作 - 离开
ctx.Provider
时,执行popProvider
方法,类比出栈操作
每次执行pushProvider
时将context._currentValue
更新为当前值:
function pushProvider(context, newValue) {
context._currentValue = newValue;
}
同理,popProvider
执行时将context._currentValue
更新为上一个context._currentValue
:
function popProvider(context) {
context._currentValue = /* 上一个context value */
}
该如何表示上一个值呢?我们可以增加一个全局变量prevContextValue
,用于保存「上一个同类型的context._currentValue」:
let prevContextValue = null;
function pushProvider(context, newValue) {
// 保存上一个同类型context value
prevContextValue = context._currentValue;
context._currentValue = newValue;
}
function popProvider(context) {
context._currentValue = prevContextValue;
}
在pushProvider
中,执行如下语句前:
context._currentValue = newValue;
context._currentValue
中保存的就是「上一个同类型的context._currentValue」,将其赋值给prevContextValue
。
以下面代码举例:
代码语言:javascript复制const ctx = createContext(0);
function App() {
return (
<ctx.Provider value={1}>
<Cpn />
</ctx.Provider>
);
}
进入ctx.Provider
时:
prevContextValue
赋值为0(context
实例化时传递的默认值)context._currentValue
赋值为1(当前值)
当<Cpn />
消费ctx
时,取得的值就是1。
离开ctx.Provider
时:
context._currentValue
赋值为0(prevContextValue
对应值)
但是,我们当前的实现只能应对一层ctx.Provider
,如果是多层ctx.Provider
嵌套,我们不知道沿途ctx.Provider
对应的prevContextValue
。
所以,我们可以增加一个栈,用于保存沿途所有ctx.Provider
对应的prevContextValue
:
const prevContextValueStack = [];
let prevContextValue = null;
function pushProvider(context, newValue) {
prevContextValueStack.push(prevContextValue);
prevContextValue = context._currentValue;
context._currentValue = newValue;
}
function popProvider(context) {
context._currentValue = prevContextValue;
prevContextValue = prevContextValueStack.pop();
}
其中:
- 执行
pushProvider
时,让prevContextValue
入栈 - 执行
popProvider
时,让prevContextValue
出栈
至此,完成了React Context
的核心逻辑,其中pushProvider
三行代码,popProvider
两行代码。
两个有意思的点
关于Context
的实现,有两个有意思的点。
第一个点:这个实现太过简洁(核心就5行代码),以至于让人严重怀疑是不是有bug
?
比如,全局变量prevContextValue
用于保存「上一个同类型的context._currentValue」,如果我们把不同context
嵌套使用时会不会有问题?
在下面代码中,ctxA
与ctxB
嵌套出现:
const ctxA = createContext('default A');
const ctxB = createContext('default B');
function App() {
return (
<ctxA.Provider value={'A0'}>
<ctxB.Provider value={'B0'}>
<ctxA.Provider value={'A1'}>
<Cpn />
</ctxA.Provider>
</ctxB.Provider>
<Cpn />
</ctxA.Provider>
);
}
当离开最内层ctxA.Provider
时,ctxA._currentValue
应该从'A1'
变为'A0'
。考虑到prevContextValue
变量的唯一性以及栈的特性,ctxA._currentValue
会不会错误的变为'B0'
?
答案是:不会。
JSX
结构的确定意味着以下两点是确定的:
ctx.Provider
的进入与离开顺序- 多个
ctx.Provider
之间嵌套的顺序
第一点保证了当进入与离开同一个ctx.Provider
时,prevContextValue
的值始终与该ctx
相关。
第二点保证了不同ctx.Provider
的prevContextValue
被以正确的顺序入栈、出栈。
第二个有意思的点:我们知道,Hook
的使用有个限制 —— 不能在条件语句中使用hook
。
究其原因,对于同一个函数组件,Hook
的数据保存在一条链表上,所以必须保证遍历链表时,链表数据与Hook
一一对应。
但我们发现,useContext
获取的其实并不是链表数据,而是ctx._currentValue
,这意味着useContext
其实是不受这个限制影响的。
总结
以上五行代码便是React Context
的核心实现。在实际的React
源码中,Context
相关代码远不止五行,这是因为他与其他特性耦合在一块,比如:
- 性能优化相关代码
SSR
相关代码
所以,当我们面对复杂代码时,不要轻言放弃。仔细分析下,没准儿核心代码只有几行呢?