实现 reactive

653 阅读3分钟

学习 vue3 reactive 响应式原理的6小时中我收获了什么?

本来是想学习 vitest 的,然后搭建好了一切发现 vitest 真的很好上手,之前没有用过其他测试,所以写一些语法还需要翻文档,为了方便单元测试,我就突发奇想 实现一个 reactive ,走一走这个测试,话不多说,分享一下我的收获

学习的github 仓库 mini-vue

使用的测试工具vitest 语法和 jest 很像 不太明白看的jest

vitest我的配置

import vue from '@vitejs/plugin-vue';
import { configDefaults, defineConfig } from 'vitest/config';
import { resolve } from 'path';

export default defineConfig({
  resolve: {
    alias: {
      '@': `${resolve(process.cwd())}/src`,
    },
    extensions: ['tsx', '.ts', '.js'],
  },
  test: {
    globals: true,
    // ...
    exclude: [...configDefaults.exclude, 'package/template/*'],
    coverage: {
      provider: 'c8',
      reporter: ['html', 'json', 'text'],
      reportsDirectory: './tests/unit/coverage', //
    },
  },
  plugins: [vue()],
});

其中test 就是 配置测试工具的配置项 这些文档上都有 注意配置 全局 globals 之后 tsconfig.json 需要设置

"types":["vitest/globals"] 配置以后 就可以直接使用 vitest 的函数

WeekMap

  • 可储存与对象相关的值,但不会强制保留在内存中,WeekMap 对健是弱引用
  • 是不可迭代的
  • 会被垃圾回收

经典用例是私有信息

export const Example = (() => {
  const privateMap = new WeakMap();

  return class Example {
    constructor() {
      privateMap.set(this, 0);
    }

    incrementCount() {
      const result = privateMap.get(this) + 1;
      privateMap.set(this, result);
      return result;
    }

    showCounter() {
      console.log(`Counter is ${privateMap.get(this)}`);
    }
  };
})();

const e1 = new Example();
e1.incrementCount();

const e2 = new Example();
e2.incrementCount();
e2.incrementCount();

e1.showCounter();  // 1
e2.showCounter();  // 2

Reflect Proxy

把这两个放一块

  • Reflect: 反射对象,包含了对象的基本操作想对应的方法
  • Proxy: 代理对象 可以为要处理的操作 提供一个或者多个劫持函数

在设计中 ReflectProxy 应该结合使用,Reflect为编程者构造对象并与之交互提供了大量使用特性,Proxy为 Javascript 提供了最佳外观模式 ,在 Reactive 中也是搭配起来使用

来一块简单示例

const target = { a: 1, word: 'sakana',arr: [1] };

console.log(Reflect.get(target, 'a')); // 反射

const proxy = new Proxy(target, {
  get(target, name, receiver) {
    let value = Reflect.get(target, name, receiver);
    if (value && typeof value.toUpperCase === 'function') {
      value = value.toUpperCase();
    }
    return value;
  },
  set(target, name, value, receiver) {
    return Reflect.set(target, name, value, receiver);
  },
});

console.log(target.a); // 1
console.log(proxy.a); // 1
console.log(target === proxy); // false
console.log('----', target.word); // sakana
console.log('----proxy', proxy.word); // SAKANA

proxy.a = 2;
console.log('------------a', proxy.a); // 2

proxy.arr.push(2)
console.log(proxy.arr); // [ 1, 2 ]

从上面的示例中可以梳理几点

  • Proxy 返回的是一个新的包装对象
  • 自动处理数组的方法

还是看源码的时候,学习的,不多,但有用,接下来就是 reactive 了,做好了前期铺垫

Reactive

我是在vue3 项目中 node_modules\vue\dist\vue.global.js 看的源码 , 结合 mini-vue一起

目标:Reactive

reactive 函数

主要还是创建代理对象

target 是要包装的对象

reactiveMap 缓存

mutableHandlers Proxy 的配置项

export function reactive(target) {
  return createReactiveObject(target, reactiveMap, mutableHandlers);
}

看一下createReactiveObject核心实现

创建了一个 reactiveMap 通过传递过来 使用map检查缓存是否有啦,有就返回这个咯

然后就是使用Proxy代理对象 其实就是这么多一点 主要还是在配置项 处理

export const reactiveMap = new WeakMap();

function createReactiveObject(target, proxyMap, baseHandlers) {
  // 检查是否存在 
  const existingProxy = proxyMap.get(target);
  if (existingProxy) {
    return existingProxy;
  }

  // 返回一个新的proxy
  const proxy = new Proxy(target, baseHandlers);
  // 存放proxy
  proxyMap.set(target, proxy);
  return proxy;
}

mutableHandlers 中 createGetter 函数

分析一下 这里几个判断哥们是有点昏的 大意应该是判断 key 是否是调用函数某些能否匹配上 返回对应的结果

然后就是 res 通过 Reflect 反射

使用track收集依赖 核心是通过Dep

判断res 是否是一个对象 是的话 总之就是在 通过Proxy包装一下

export function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly;
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly;
    } else if (key === ReactiveFlags.IS_SHALLOW) {
      return shallow;
    } else if (
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReadonlyMap
          : reactiveMap.get(target))
    ) {
      return target;
    }

    const res = Reflect.get(target, key, receiver);

    if (!isReadonly) {
      // get收集依赖
      track(target, 'get', key);
    }

    if (shallow) {
      return res;
    }
    if (isObject(res)) {
      // 如果是对象 那么进行包装 proxy
      return isReadonly ? readonly(res) : reactive(res);
    }

    return res;
  };
}

mutableHandlers 中 createSetter 函数

使用Reflect 返回 result 在触发 执行相关函数

export function createSetter(shallow = false) {
  return function set(target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver);

    // 在触发 set 的时候进行触发依赖
    trigger(target, 'set', key);
    return result;
  };
}

单元测试

import { isReactive, reactive } from '../src/reactive';
import { test, describe, it, expect } from 'vitest';

describe('reactive', () => {
  test('Object', () => {
    const original = { foo: 1 };
    const observed = reactive(original);
    expect(observed).not.toBe(original); // 比较两个对象是不是不相同
    expect(isReactive(observed)).toBe(true);
    expect(isReactive(original)).toBe(false);
    expect(observed.foo).toBe(1);
    expect('foo' in observed).toBe(true);
    expect(Object.keys(observed)).toEqual(['foo']);
  });

  test('nested isReactive', () => {
    const original = {
      nested: {
        foo: 1,
      },
      array: [{ bar: 2 }],
    };
    const observed = reactive(original);
    expect(isReactive(observed.nested)).toBe(true);
    expect(isReactive(observed.array)).toBe(true);
    expect(isReactive(observed.array[0])).toBe(true);
  });
});

写测试的时候 帮我定位了两处问题,会告诉我 预期 和 实际的值,帮我快速定位的bug位置

总结

学习了三种API,查阅资料知道他们的特性,学会配置vitest给自己的reactive进行单元测试,奈何文笔太差,表达不出太多感情,反正写完很爽!