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