共计 4287 个字符,预计需要花费 11 分钟才能阅读完成。
大家好,我卡颂。
很多我的项目的源码非常复杂,让人望而生畏。但在打退堂鼓前,咱们应该思考一个问题:源码为什么简单?
造成源码简单的起因不外乎有三个:
- 性能自身简单,造成代码简单
- 编写者功力不行,写的代码简单
- 性能自身不简单,但同一个模块耦合了太多功能,看起来简单
如果是起因 3,那理论了解起来其实并不难。咱们须要的只是有人能帮咱们剔除无关性能的烦扰。
React Context
的实现就是个典型例子,当剔除无关性能的烦扰后,他的外围实现,仅需 5 行代码。
本文就让咱们看看 React Context
的外围实现。
欢送围观朋友圈、退出人类高质量前端交换群,带飞
简化模型
Context
的残缺工作流程包含 3 步:
- 定义
context
- 赋值
context
- 生产
context
以上面的代码举例:
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
。
以上面代码举例:
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
相干代码
所以,当咱们面对简单代码时,不要轻言放弃。仔细分析下,没准儿外围代码只有几行呢?