React Context的核心实现,就5行代码

103 阅读5分钟

大家好,我卡颂。

很多项目的源码非常复杂,让人望而却步。但在打退堂鼓前,我们应该思考一个问题:源码为什么复杂?

造成源码复杂的原因不外乎有三个:

  1. 功能本身复杂,造成代码复杂
  2. 编写者功力不行,写的代码复杂
  3. 功能本身不复杂,但同一个模块耦合了太多功能,看起来复杂

如果是原因3,那实际理解起来其实并不难。我们需要的只是有人能帮我们剔除无关功能的干扰。

React Context的实现就是个典型例子,当剔除无关功能的干扰后,他的核心实现,仅需5行代码

本文就让我们看看React Context的核心实现。

欢迎围观朋友圈、加入人类高质量前端交流群,带飞

简化模型

Context的完整工作流程包括3步:

  1. 定义context
  2. 赋值context
  3. 消费context

以下面的代码举例:

const ctx = createContext(null);


function App() {
return (
<ctx.Provider value={1}>
<Cpn />
</ctx.Provider>
);
}



                                function Cpn() {
                                    const num = useContext(ctx);
                                        return &lt;div&gt;{num}&lt;/div&gt;;
                                        }</code></pre><p>其中:</p><ul><li><code>const ctx = createContext(null)</code> 用于定义</li><li><code>&lt;ctx.Provider value={1}&gt;</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>&lt;ctx.Provider&gt;</code>包裹<code>&lt;Cpn /&gt;</code></p><pre><code class="js">function App() {
                                                                          return (
                                                                                  &lt;ctx.Provider value={1}&gt;
                                                                                              &lt;Cpn /&gt;
                                                                                                      &lt;/ctx.Provider&gt;
                                                                                                          );
                                                                                                          }</code></pre><p>在实际项目中,消费<code>ctx</code>的组件(示例中的<code>&lt;Cpn/&gt;</code>)可能被多级<code>&lt;ctx.Provider&gt;</code>包裹,比如:</p><pre><code class="js">const ctx = createContext(0);
                                                                                                          
                                                                                                          function App() {
                                                                                                              return (
                                                                                                                  &lt;ctx.Provider value={1}&gt;
                                                                                                                        &lt;ctx.Provider value={2}&gt;
                                                                                                                                &lt;ctx.Provider value={3}&gt;
                                                                                                                                          &lt;Cpn /&gt;
                                                                                                                                                  &lt;/ctx.Provider&gt;
                                                                                                                                                          &lt;Cpn /&gt;
                                                                                                                                                                &lt;/ctx.Provider&gt;
                                                                                                                                                                      &lt;Cpn /&gt;
                                                                                                                                                                          &lt;/ctx.Provider&gt;
                                                                                                                                                                            );
                                                                                                                                                                            }</code></pre><p>在上面代码中,<code>ctx</code>的值会从0(默认值)逐级变为3,再从3逐级变为0,所以沿途消费<code>ctx</code><code>&lt;Cpn /&gt;</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 (
                                                                                                                                                                                                                  &lt;ctx.Provider value={1}&gt;
                                                                                                                                                                                                                              &lt;Cpn /&gt;
                                                                                                                                                                                                                                      &lt;/ctx.Provider&gt;
                                                                                                                                                                                                                                          );
                                                                                                                                                                                                                                          }</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>&lt;Cpn /&gt;</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 (
                                                                                                                                                                                                                                                                      &lt;ctxA.Provider value={'A0'}&gt;
                                                                                                                                                                                                                                                                            &lt;ctxB.Provider value={'B0'}&gt;
                                                                                                                                                                                                                                                                                    &lt;ctxA.Provider value={'A1'}&gt;
                                                                                                                                                                                                                                                                                              &lt;Cpn /&gt;
                                                                                                                                                                                                                                                                                                      &lt;/ctxA.Provider&gt;
                                                                                                                                                                                                                                                                                                            &lt;/ctxB.Provider&gt;
                                                                                                                                                                                                                                                                                                                  &lt;Cpn /&gt;
                                                                                                                                                                                                                                                                                                                      &lt;/ctxA.Provider&gt;
                                                                                                                                                                                                                                                                                                                        );
                                                                                                                                                                                                                                                                                                                        }</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>