学习 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: 代理对象 可以为要处理的操作 提供一个或者多个劫持函数
在设计中 Reflect 和 Proxy 应该结合使用,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进行单元测试,奈何文笔太差,表达不出太多感情,反正写完很爽!