关于前端:React-Context的核心实现就5行代码

29次阅读

共计 4287 个字符,预计需要花费 11 分钟才能阅读完成。

大家好,我卡颂。

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

造成源码简单的起因不外乎有三个:

  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 <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工作流程的三个步骤其实能够概括为:

  1. 实例化 context,并将默认值defaultValue 赋值给context._currentValue
  2. 每遇到一个同类型 context.Provier,将value 赋值给context._currentValue
  3. 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 嵌套应用时会不会有问题?

在上面代码中,ctxActxB 嵌套呈现:

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构造的确定意味着以下两点是确定的:

  1. ctx.Provider的进入与来到程序
  2. 多个 ctx.Provider 之间嵌套的程序

第一点保障了当进入与来到同一个 ctx.Provider 时,prevContextValue的值始终与该 ctx 相干。

第二点保障了不同 ctx.ProviderprevContextValue被以正确的程序入栈、出栈。

第二个有意思的点:咱们晓得,Hook的应用有个限度 —— 不能在条件语句中应用hook

究其原因,对于同一个函数组件,Hook的数据保留在一条链表上,所以必须保障遍历链表时,链表数据与 Hook 一一对应。

但咱们发现,useContext获取的其实并不是链表数据,而是 ctx._currentValue,这意味着useContext 其实是不受这个限度影响的。

总结

以上五行代码便是 React Context 的外围实现。在理论的 React 源码中,Context相干代码远不止五行,这是因为他与其余个性耦合在一块,比方:

  • 性能优化相干代码
  • SSR相干代码

所以,当咱们面对简单代码时,不要轻言放弃。仔细分析下,没准儿外围代码只有几行呢?

正文完
 0