理解Vue

总览

Vue3 的基本结构

采用 Monorepo 模式(多组件放在一个 Repo 中), 在 /packages/ 中存储所有的模块.

模块分为几类:

  • 编译时(/package/compiler-*)
    • compiler-core: 与平台无关的编译器核心
    • compiler-dom: 基于 compiler-core 解析 <template> 标签并编译为 render 函数
    • compiler-sfc: 基于 compiler-domcompiler-core 解析 SFC (单文件组件, 通俗理解就是 .vue 文件) 编译为浏览器可执行的 JavaScript
    • compiler-ssr: 服务端渲染的编译模块
  • 运行时(/package/runtime-*)
    • reactivity: 实现响应式
    • runtime-core: 基于 reactivity 实现运行时核心
    • runtime-dom: 基于 runtime-core 实现针对浏览器的运行时. 包括DOM API, 属性, 事件处理等
  • 其他
    • template-explorer: 用于调试编译器输出的开发工具
    • shared: 多个包之间共享的内容
    • vue: 完整版本,包括运行时和编译器

依赖关系

                                 +---------------------+
                                 |                     |
                                 |  @vue/compiler-sfc  |
                                 |                     |
                                 +-----+--------+------+
                                       |        |
                                       v        v
                   +---------------------+    +----------------------+
                   |                     |    |                      |
     +------------>|  @vue/compiler-dom  +--->|  @vue/compiler-core  |
     |             |                     |    |                      |
+----+----+        +---------------------+    +----------------------+
|         |
|   vue   |
|         |
+----+----+        +---------------------+    +----------------------+    +-------------------+
     |             |                     |    |                      |    |                   |
     +------------>|  @vue/runtime-dom   +--->|  @vue/runtime-core   +--->|  @vue/reactivity  |
                   |                     |    |                      |    |                   |
                   +---------------------+    +----------------------+    +-------------------+

学习路线

根据模块依赖关系, 路线为: reactivity -> runtime-core -> runtime-dom -> compiler. 重点是 runtime-*

代码分析步骤:

  1. 查看单元测试(位于packages/**/__tests__/)
  2. 根据单元测试了解模块实现的功能
  3. 跟着单元测试的了解模块功能, 了解模块功能时: 先看导出(模块是什么), 再看模块被谁导入(为什么被需要), 最后看导出部分对应的实现(怎么样实现)

参考 repo

  • cuixiaorui/mini-vue: 用来学习
  • vuejs/core: 用来验证

Reactivity 的基本流程

Reactivity 模块是运行时的最底层, 负责实现响应式, 位于: mini-vue/packages/reactivity

reactive 的基本流程

reactiveReactivity 的基础. 负责实现对象的响应式, 并向上提供调用时方法. 基本思想就是借助 ES6 的 Proxy 自定义 get & set

  1. 转到 mini-vue/../__tests__/reactive.spec.ts, 发现测试的主要目的是看 reactive 构造方法.

  2. 转到 mini-vue/../src/reactive.ts, 发现定义了 reactive, readonly 等方法, 这些方法都交由 createReactiveObject 处理.

    观察 createReactiveObject, 可以得到三个调用参数意义:

    • target: 要被代理的值

    • proxyMap: 不同类型的工厂函数有不同的全局 proxyMap, 这意味着该变量可能会存储所有代理的某类型变量. 根据

      const existingProxy = proxyMap.get(target);
      if (existingProxy) {
          return existingProxy;
      }

      可以验证想法, 其在 createReactiveObject 的目的就是持久化 Proxy 防止重复创建代理

    • baseHandlers: 根据

      const proxy = new Proxy(target, baseHandlers);

      得出该方法就是 Proxy(MDN) 的 get & set 对象. 不同类型的 Proxy 有不同的 baseHandlers

  3. 转到 mini-vue/../src/baseHandlers.ts 发现模块主要是提供不同的 get & set 而这些都是由两个 create 函数实现的, 尝试理解

    • createGetter 应该返回一个 handler.get(MDN) 实现. 可以看到这个函数上有一堆类型判断的方法, 然后做了两步

      • 通过 Reflect.get(MDN) 获取属性
      • 通过 track 进行依赖收集, 这部分后面再看

      最后返回获取结果. 整个 get 感觉和原生方法相比就是多了个类型判断和 track, 大部分的响应式都是依赖这个 track 实现的

    • createSetter 更加简单, 看起来就是在实现 handler.set(MDN) 的基础上多了个 trigger

    到目前位置这个只有 tracktrigger 是不清楚的, 这两个函数在 effect 等部分做依赖收集的, 可以先不管. 其他部分就是原生功能调用与权限管理

effect 的基本流程

如果让我实现 effect 我会怎么实现呢? 我先想到的是利用编译原理等魔法对代码做静态分析, 找到所用响应式对象, 在响应式对象的 set 上挂上函数. 但是, JavaScript 是个动态语言, 这完全没法挂啊! 只能在运行时动态解析.

Vue 的实现就比较流畅. 既然我 effect 要立即执行一遍函数, 那为啥不在执行前后做下 Flag, 一旦 Proxy 的 get 被调用, 让 get 检查一下是不是在 effect 执行阶段, 若是就把函数注册到这个响应式对象上😎

  1. 转到 mini-vue/../__tests__/reactive.spec.ts 看到 effect 的主要功能是立即执行函数并在响应式数据发生改变时, 去执行 effect 注册的函数

  2. 转到 mini-vue/../src/effect.tseffect 函数的实现. 看到这里有熟悉的 effect, track, trigger

    1. effect 函数将传入函数包装为 ReactiveEffect 对象, 合并配置, 执行 run 函数, 构造 runner 并返回(用于后期调用)

    2. ReactiveEffect

      • active: 根据 run, stop 函数和测试文件中的 it("stop") 断言可以推出其是用来开关 effect 功能的
      • deps: 根据 tracktrigger 对其调用可以判断其是用来记录函数对应依赖的
      • run: 对 effect 注册函数的包装, 在执行函数前后打入 shouldTrack 标记, 并将 activeEffect 标记为要执行的 ReactiveEffect 好让 get 知道哪个 effect 在跑
    3. track 函数: 在 reactiveget 中调用

      track(target, "get", key);

      track 发现自己处于 effect 阶段时会先检查自己所在对象有没有创建 attribute - effect 函数heap 的 map, 如果每就创建, 然后看 map 上有没有记录当前属性, 如果没有, 就建立依赖的 set 并交由 trackEffects 加入并在 ReactiveEffect 上也做记录.

    4. trigger 函数: 在 reactiveset 中调用

      先找到对应 attributeeffect 依赖, 去重, 根据配置延迟或立即支持 effect

总结

  • reactive 的流程: 传入对象, 持久化, 绑定 baseHandlers 做权限管理与依赖收集
  • effect 的流程: 将传入函数包装为对象, 立即执行函数并做好标记, 在执行时收集依赖. 每当 reactive 被调用时就 tigger 收集的 effect, 并二次收集依赖

问题

  • 所有的依赖收集都是基于 get, 这样的 effect 存在问题

    it('should observe basic properties', () => {
      let dummy, flag = false;
      const counter = reactive({ num: 0 });
      effect(() => {
        if (!flag) {
          dummy = -1;
        } else {
          dummy = counter.num;
        }
      });
    
      expect(dummy).toBe(-1);
      flag = true;
      counter.num = 2;
      expect(dummy).toBe(2); // Except 2, Received -1
    });

    不只是 mini-vue, vue/core 的单元测试也存在这个问题. 但是在 Vue 代码中并不会出现无法追踪依赖的问题, 看来还有一些隐藏的优化没有找到

Runtime-core 的基本流程

runtime-core 依赖 Reactivity 为 runtime 提供服务. 可以通过观察 Vue 文件的运行观察 runtime-core 的基本流程

文件基本结构

  1. 转到 mini-vue/packages/vue/example/helloWorld/ 的文件夹了解 vue 的基本工作流程

  2. 转到 mini-vue/../helloWorld/index.html, 只有个 div#rootscript

  3. 转到 mini-vue/.../helloWorld/main.js

    import { createApp } from '../../dist/mini-vue.esm-bundler.js';
    import App from './App.js';
    
    const rootContainer = document.querySelector('#root');
    createApp(App).mount(rootContainer);

    引入了创建根组件的 createApp 与根组件 App, 查找了 html 文件中声明的挂载点, 然后通过 createApp(App) 打包根组件再将打包后结果挂载

  4. 转到 mini-vue/../helloWorld/App.js 发现定义了两个 vue2 风格的组件对象

    {
      name: 'App', // 组件名
      setup() {}, // setup 方法
    
      render() { // 渲染方法
        return h('div', { tId: 1 }, [h('p', {}, '主页'), h(HelloWorld)]);
      },
    };
    • 前面有提到: compiler-dom<template> 标签解析并编译为 render 函数. 在这里为了不追踪 compiler-dom 的行为, 我们直接将 render 给出

    • h 为渲染函数, 参数分别是: 组件的 ElementType, 配置, 子组件数组, 可以看到, 这里第一个子组件是一个 <p> 第二个是一个组件

    • 可以在对象中使用 render, 也可以让 setup 返回 render 方法, 即

      {
        name: 'App',
        setup() {
          return function() {
            return h('div', { tId: 1 }, [h('p', {}, '主页'), h(HelloWorld)]);
          }
        },
      };.
  5. createApp 调用关系比较复杂, 直接使用 dev-tools 观察执行过程. 打开一个 http 服务器并转到 dev-tools下, 找到 createApp.js 并打下断点

  6. createApp 方法接受根组件配置对象 App 直接包了个对象, 有

    • _componment = App
    • mount 方法, 看语义, 这个方法接收挂载点, 将根组件创建为 VNode 并挂载到挂载点(main.js 中的 rootContainer), 执行完后 main.js 就结束了

    我们需要继续分析的就是 VNode 的创建过程与 render 的挂载过程

组件初始化过程

  1. 单步进入 createVNode 发现其声明了个 vnode.

    将传入对象(rootComponent / App) 作为 vnode.type

    vnode 上合并对象并配置 shapeFlag 用于标记类型

    之后调用 normalizeChildren 并返回对象

    • 进入 normalizeChildren 看起来是作了 slot 特判
  2. 单步进入 render, 其接收了处理后的 vnode 与挂载点 rootContainer 然后将参数直接交给 patch, 可以猜到 patch 会是一个很通用的函数

    • 单步进入 patch, 其接收 n1 = null, n2 = vnode, container.

      解构出了n2type = AppshapeFlag,

      通过预定义的 Symbol 判断对象类型, 进入 default,

      通过位运算判断 shapeFlag 类型, 被识别为组件 (而不是像 h('p', {}, '主页') 一样的 Element) 执行 processComponent

      • 单步进入 processComponent,

        函数做了一个判断: 如果没有 n1 就认为 n2 还没有被挂载就挂载 n2 否则更新 n2

        • 单步进入 mountComponent, 其接收了 vnode 与挂载点

          vnode 转换为实例 instance, 执行 setupComponent 处理 instance

          • 单步进入 setupComponent 发现其只是处理了 propslot 然后交给 setupStatefulComponent 继续配置

            • 单步进入 setupStatefulComponent, 其接收 instance

              instance.ctx 配置了 PublicInstanceProxyHandlers 代理(后面分析)

              提取 Component = APP, setup = APP.setup

              如果 setup 不存在就直接 finishComponentSetup

              否则用 setCurrentInstance 打标记, 为 setup 传入参数并获取执行结果, 执行 handleSetupResult 处理结果

              • 单步进入 handleSetupResult 该函数对 setup 结果执行判断

                如果是 function 说明是导出了 render 函数, 将 render 赋值到 instance.render

                否则导出的对象存入 isntance.setupState

                最后执行 finishComponentSetup 与无 setup 的情况汇合

              • 单步进入 finishComponentSetup 其接收 instance

                instance 上没有 render 就尝试从 template 编译结果上获取并存入 instrance.render

          • 单步进入 setupRenderEffect 发现其定义绑定了一个 componentUpdateFn 函数

            • 打断点并进入 componentUpdateFn 函数

              如果组件没有被挂载, 获取子节点, 获取 instance 的 Proxy, 构建子节点 subTree 并递归 patch, 当 patch 到 Element 时调用 processElement 挂载节点

              否则更新节点(后面分析)

组件更新过程

为组件创建响应式并将 reavtive 导出到全局

{
  name: 'HelloWorld',
  setup() {
    const count = ref(10);
    window.count = count;
    return { count };
  },
  render() {
    return h('div', { tId: 'helloWorld' }, `hello world: count: ${this.count}`);
  },
};

在 dev-tools 中修改 count.value 根据输出来自 effect.ts 进入文件并为 run 函数打上断点, 再次修改值, 发现 run 函数实际上就是执行了当时的 componentUpdateFn, 为 componentUpdateFn 中已挂载的判断部分打上断点

  1. 在断点处查看调用栈, 确定函数就是因为 ref 修改而引发的

  2. 在执行修改前先判断有没有 nextTrick 需要执行

  3. 获取新节点的 vnode

  4. 将老节点子树复制到新节点

  5. 触发生命周期函数

  6. patch 新节点

    单步进入 patch, 接受老节点 n1 新节点 n2 这次更新的是一个 Element 于是进入 ShapeFlags.ELEMENT, 进入 processElement

    • 单步进入 processElement, 这次老节点已经挂载, 直接走更新程序
      • 单步进入 updateElement 该函数分别对比了 props 与 子节点并更新
  7. 触发生命周期函数

总结

graph TB

init((初始化组件)) --> createAPp[将App交给createApp, 将App包装为vnode] --- norm1[将vnode应用normalizeChildren配置, 交给render渲染]  --> renderdispatch[render直接交给patch] --> check[patch检查类型] --为组件--> processComponent[交给processComponent判断状态] --为新节点--> mountComponent[执行mountComponent挂载vnode: 构造instance, 运行 setup, 获取 render] --> effect[注册render的effect] --> run[执行effect, 检测是否挂载]  --没有--> patch2(递归patch子节点)

update((reactive更新)) -.-> run[执行effect, 检测是否挂载] -.-挂载了-.-> newvnode[构造新vnode, diff检查, 复制属性] -.-> patch2 ==> check

check ==为Element==> mountDir(直接修改DOM)

实现 Reactivity

环境搭建

  • 目录结构

    .
    ├── jest.config.js
    ├── package.json
    ├── packages
    │   └── reactivity
    │       ├── index.ts # 入口文件
    │       └── __tests__ # 测试文件
    │           └── index.spec.ts
    ├── README-EN.md
    ├── README.md
    └── tsconfig.json # tsc --init

  • 依赖: typescript / @types/node / jest / ts-jest / @types/jest

构建基本 effectreactive

TDD

TDD(Test-Driven Development), 是敏捷开发中的一项核心实践和技术, 也是一种设计方法论. TDD的原理是在开发功能代码之前, 先编写单元测试用例代码, 测试代码确定需要编写什么产品代码. TDD虽是敏捷方法的核心实践.

构建基本的 reactive

需求: 最简单的 reactive, 输入对象并输出对象的代理. 代理对象修改时原对象同步修改

  1. 测试
    it('Should different', () => {
      const origin = { foo: 1 };
      const observed = reactive(origin); // 输入对象并返回代理对象
      expect(observed).not.toBe(origin); // observed 和原来的不是一个对象
      observed.foo = 3;
      expect(observed.foo).toBe(3); // 两者同步修改
      expect(origin.foo).toBe(3);
    });
  2. 实现 只需要为对象配置一个普通代理
    export function reactive(origin) {
      // 就是给一个对象, 返回一个 new Proxy
      return new Proxy(origin, {
        // 语法见 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/get
        get(target, key, receiver) {
          return Reflect.get(target, key, receiver);
        },
        // 语法见 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/set
        set(target, key, value, receiver) {
          return Reflect.set(target, key, value, receiver);
        },
      });
    }
    唯一的难点就是 Proxy 语法
  3. 重构: 无

构建基本的 effect

需求: 1. 输入函数, 执行函数, 当函数中被 [GET] 的响应式对象发生变化时重新执行函数 2. 返回一个函数 runner, 当执行 runner 时执行 effect 传入的函数

需求分析: 1. 为什么是函数中被 [GET] 的响应式对象变化时重新执行函数, [SET] 不行吗? 不行, 响应式对象被 [SET] 后如果执行了函数, 响应式对象会被重新 [SET], 那么上一次 [SET] 就没用了. 同时如果函数中其他变量不变只有响应式对象被 [SET] 此时执行函数并不会使得函数中变量值发生变化(毕竟变化的响应式变量没有被 [GET]), 不会产生 sideEffect. 2. 执行流程: 开始执行函数 -> [GET] 响应式对象 -> 结束执行函数 -> 当响应式对象被 [SET] -> 执行函数 可以发现只需要让响应式对象知道当自己变化时哪些 effect 需要执行就可以了, 至于 effect 知不知道响应式对象是谁那无所谓. 可以在函数执行期间执行依赖收集, 为 [GET] 的响应式对象注册 Effect Function, 在响应式对象修改时执行其注册的 Effect Function.

 

  1. 测试:
    describe('Effect test', () => {
      it('Should sync', () => {
        const origin = { foo: 1 };
        const observed = reactive(origin);
        let bar;
        const runner = effect(() => {
          bar = observed.foo;
        });
        expect(observed.foo).toBe(1); // origin -> observed
        expect(bar).toBe(1); // 立即执行 fn
        observed.foo = 2; // 修改有 [GET] 的响应式对象
        expect(observed.foo).toBe(2); // 响应式对象变化
        expect(origin.foo).toBe(2); // 原对象变化
        expect(bar).toBe(2); // 执行函数
      });
    
      it('Should return runner', () => {
        const origin = { foo: 1 };
        const observed = reactive(origin);
        console.info = jest.fn(); // 劫持 console.info
        let bar;
        const runner = effect(() => {
          console.info('I RUN');
          bar = observed.foo;
        });
        expect(console.info).toBeCalledTimes(1); // 立即执行 fn, console.info 被调用 1 次
        observed.foo = 2;
        expect(console.info).toBeCalledTimes(2); // 响应式对象发生变化执行 fn, console.info 被调用 2 次
        runner(); // 手动调用 runner, console.info 被调用 3 次
        expect(console.info).toBeCalledTimes(3);
      });
    });
  2. 实现 利用 targetMap 实现响应式对象 -> Key -> Effective Function 的映射. 导出 tracktrigger 用于收集与触发依赖
    // target: Object => keyMap:(string=>Set)
    // keyMap: string => Set
    const targetMap: Map<any, Map<string, Set<EffectReactive>>> = new Map();
    let activeEffectFn: any = undefined;
    
    class EffectReactive {
      runner: (...args: any[]) => any;
    
      constructor(public fn) {
        this.runner = this.run.bind(this);
        activeEffectFn = this; // 全局注册当前正在收集依赖的 Effect
        this.run(); // 执行函数
        activeEffectFn = undefined; // 取消注册
      }
    
      run() {
        this.fn();
      }
    }
    
    export function effect(fn) {
      // 考虑到 effect 上动作很多, 我们将其抽离为 EffectFunction 函数
      return new EffectReactive(fn).runner;
    }
    
    // 依赖收集函数, 由 `[GET]` 触发, 该函数检查是否有 active 的 Effect, 有就收集依赖
    export function track(target, key) {
      if (!activeEffect) return;
      if (!targetMap.has(target)) targetMap.set(target, new Map());
      const keyMap = targetMap.get(target)!;
      if (!keyMap.has(key)) keyMap.set(key, new Set());
      const dependenceEffect = keyMap.get(key)!;
      dependenceEffect.add(activeEffect);
    }
    
    // 触发函数, 当响应式对象被 `[SET]` 时尝试触发其收集的所有 Effect
    export function trigger(target, key) {
      const keyMap = targetMap.get(target)!;
      if (!keyMap) return;
      const depSet = keyMap.get(key)!;
      if (!depSet) return;
      [...depSet].forEach((d) => d.run());
    }
    Proxy 上同步修改
    export function reactive(origin) {
      return new Proxy(origin, {
        get(target, key, receiver) {
    +     track(target, key);
          return Reflect.get(target, key, receiver);
        },
        set(target, key, value, receiver) {
    +     // 这两行顺序反了就寄了
          const res = Reflect.set(target, key, value, receiver);
    +     trigger(target, key);
          return res;
        },
      });
    }
  3. 重构: 无

构建 effectscheduler 选项 (watch)

需求: 为 effect 传入第二个参数, 参数是一个对象, 其中包含 scheduler 函数, 当构造 Effect 时执行传入的第一个函数参数, 当响应式函数变化时执行 scheduler 函数. 这与 Vue 3 的 watch 类似

需求分析: 在构造 Effect 的时候传入配置并在触发的时候判断是否有 scheduler 函数

  1. 测试

it('Shound run scheduler', () => {
  const origin = { foo: 1 };
  const observed = reactive(origin);
  let bar;
  effect(
    () => {
      bar = observed.foo;
    },
    { // 传入配置
      scheduler() {
        bar = -observed.foo;
      },
    }
  );
  expect(bar).toBe(1); // 第一次运行 fn 函数
  observed.foo = 2;
  expect(bar).toBe(-2); // 第二次运行 scheduler 函数
});

  1. 实现
  • 修改 effect 函数, 加入配置项
    export function effect(fn, options = {}) {
      return new EffectReactive(fn, options).runner;
    }
  • 修改 EffectReactive 的构造函数加载配置项
    class EffectReactive {
      runner: (...args: any[]) => any;
      scheduler: (...args: any[]) => any | undefined;
    
      constructor(public fn, options: any) {
        this.scheduler = options.scheduler;
        // ...
      }
    
      // ...
    }
  • 修改触发函数
    export function trigger(target, key) {
      // ...
      [...depSet].forEach((d) => (d.scheduler ? d.scheduler() : d.run()));
    }
  • 重构: 无

什么时候尝试抽离函数 / 对象

  1. 函数上有很多动作
  2. 函数作用范围广, 语义差

构建 effectstoponStop 选项

需求: 1. 定义一个外部函数 stop 传入 runnerrunner 不再被响应式对象 2. effect 中加入 onStop 配置, 在 stop 时调用

需求分析: 只需要将 EffectFunction 从响应式对象的依赖表中删除即可. 但是我们之前就没记录有哪些响应式对象将 EffectFunction 作为依赖..., 所以需要开一个 Set 记录这些响应式对象. 同时, 我们不需要记录依赖的对象是什么, 只需要记录 KeyMap 对应的 Set.

  1. 测试
    it('Should stop trigger', () => {
      const origin = { foo: 1 };
      const observed = reactive(origin);
      let bar;
      const runner = effect(
        () => {
          bar = observed.foo; // 立即执行
        },
        {
          onStop() {
            if (bar > 0) bar = 0; // 如果首次调用置 0
            bar--;
          },
        }
      );
      expect(bar).toBe(1); // 立即执行
      stop(runner);
      expect(bar).toBe(-1); // 停止后第一次执行为 -1
      observed.foo = 2;
      expect(bar).toBe(-1); // reactive 变化也不调用 fn
      stop(runner)
      expect(bar).toBe(-1); // 反复 stop 不反复执行 onStop
    });
  2. 实现 修正 EffectReactive
    class EffectReactive {
      runner: { // effect 只返回 runner, stop 函数需要根据 runner 找到 EffectReactive, 所以要在函数上加一个属性记录一下
        (...args: any[]): any;
        effect?: EffectReactive;
      };
      onStop: (...args: any[]) => any | undefined; // stop 回调
      deps: Set<Set<EffectReactive>>; // 收集了这个函数依赖的变量的依赖表集合
      active: boolean; // EffectReactive 是否运行 (stop 时置 0)
      // ...
    
      constructor(public fn, options: any) {
        this.runner = this.run.bind(this);
        this.runner.effect = this;
        this.onStop = options.onStop;
        this.deps = new Set();
        this.active = true;
        // ...
      }
    }
    修正依赖收集函数
    export function track(target, key) {
      //
      dependenceEffect.add(activeEffect);
      // 为当前正在依赖收集的 effect 的依赖上加入这个 key 的依赖表
      activeEffect.deps.add(dependenceEffect);
    }
    实现 stop 函数
    export function stop(runner) {
      // 不反复执行
      if (!runner.effect.active) return;
      runner.effect.active = false;
      // 找到所有收集过 effect 的变量, 将 effect 从依赖表中删除
      [...runner.effect.deps].forEach((d) => d.delete(runner.effect));
      // 执行 onStop
      runner.effect.onStop && runner.effect.onStop();
    }
  3. 重构: 无

构建 ProxyReadonly

需求: readonlyreactive 类似, 不过不支持 set

需求分析: 一个元素不支持 set 也就不可能触发依赖, 所以也没有必要做依赖收集. 所以只需要精简一下 reactive. 可以发现, 不同权限的变量只是在构造的时候采用不同的 [GET][SET] 策略. 可以将 [GET][SET] 抽离出来

  1. 测试
    it('Happy path', () => {
      const origin = { foo: 1 };
      const observed = readonly(origin);
      console.warn = jest.fn();
      expect(observed.foo).toBe(1); // 将原始对象包装为只读对象
      expect(console.warn).not.toHaveBeenCalled(); // 最开始不报错
      observed.foo = 2; // 修改, 静默失效, 报 warning
      expect(console.warn).toBeCalledTimes(1); // warning 被调用一次
      expect(observed.foo).toBe(1); // readOnly 静默失效
    });
  2. 实现 抽离 [GET][SET]
    // reactive 的 [GET]
    function get(target, key, receiver) {
      track(target, key);
      return Reflect.get(target, key, receiver);
    }
    
    // reactive 的 [SET]
    function set(target, key, value, receiver) {
      const res = Reflect.set(target, key, value, receiver);
      trigger(target, key);
      return res;
    }
    
    function getReadonly(target, key, receiver) {
      return Reflect.get(target, key, receiver);
    }
    
    function setReadonly(target, key, value, receiver) {
      console.warn('Can not set readonly');
      // 要返回一下设置结果, 如果返回 false 会抛出异常, 而我们只希望静默失效
      return true;
    }
    
    export const proxyConfig = {
      get,
      set,
    };
    
    export const proxyReadonlyConfig = {
      get: getReadonly,
      set: setReadonly,
    };
    抽离对象创建函数
    function createReactiveObject(origin, readonly = false) {
      if (readonly) return new Proxy(origin, proxyReadonlyConfig);
      return new Proxy(origin, proxyConfig);
    }
    重写 reactive 构建 readonly
    export function reactive(origin) {
      return createReactiveObject(origin);
    }
    
    export function readonly(origin) {
      return createReactiveObject(origin, true);
    }
  3. 重构: 上面的就是重构后的代码

构建其他工具函数

需求: 构建工具函数, isReadonly, isReactive.

需求分析: 只需要在 [GET] 上特判即可

  1. 测试
    it('isReadonly test', () => {
      const origin = { foo: 1 };
      const observed = readonly(origin);
      expect(isReadonly(observed)).toBe(true);
      expect(isReactive(observed)).toBe(false);
    });
    
    it('isReactive test', () => {
      const origin = { foo: 1 };
      const observed = reactive(origin);
      expect(isReadonly(observed)).toBe(false);
      expect(isReactive(observed)).toBe(true);
    });
  2. 实现 构造个枚举
    export const enum ReactiveFlag {
      IS_REACTIVE = '__v_isReactive',
      IS_READONLY = '__v_isReadonly',
    }
    实现函数
    export function isReactive(value) {
      return value[ReactiveFlag.IS_REACTIVE];
    }
    
    export function isReadonly(value) {
      return value[ReactiveFlag.IS_READONLY];
    }
    修改 [GET]
    const reactiveFlags = {
      [ReactiveFlag.IS_REACTIVE]: true,
      [ReactiveFlag.IS_READONLY]: false,
    };
    
    const readonlyFlags = {
      [ReactiveFlag.IS_REACTIVE]: false,
      [ReactiveFlag.IS_READONLY]: true,
    };
    
    function get(target, key, receiver) {
      if (Object.keys(reactiveFlags).find(d=>d===key)) return reactiveFlags[key];
      // ...
    }
    
    function getReadonly(target, key, receiver) {
      if (Object.keys(readonlyFlags).find(d=>d===key)) return readonlyFlags[key];
      // ...
    }
  3. 重构: 无