前言
VUE3各个模块依赖如下,其中VUE3响应式实现依赖于reactivity模块,我将在本文中使用TDD( Test-driven development 测试驱动开发 )的方式来实现一个自己的Reactivity模块
+---------------------+
| |
| @vue/compiler-sfc |
| |
+-----+--------+------+
| |
v v
+---------------------+ +----------------------+
| | | |
+------------>| @vue/compiler-dom +--->| @vue/compiler-core |
| | | | |
+----+----+ +---------------------+ +----------------------+
| |
| vue |
| |
+----+----+ +---------------------+ +----------------------+ +-------------------+
| | | | | | |
+------------>| @vue/runtime-dom +--->| @vue/runtime-core +--->| @vue/reactivity |
| | | | | |
+---------------------+ +----------------------+ +-------------------+
实现一个reactive
传入一个对象或者数组,reactive函数会返回一个响应式代理对象。 在实现具体代码之前,我们利用TDD的思想,先将所需的测试写出
// reactive.spec.ts
describe("reactive", () => {
it("happy path", () => {
const original = { foo: 1 }
const observer = reactive(original)
expect(original).not.toBe(observer)
expect(observer.foo).toBe(1)
})
})
我们现声明一个original对象,然后再用reactive函数将他包裹,original变成了一个响应式对象,我们期望original与observer不相等,并且observer对象上也有foo这个属性,值为1。
我们知道,VUE3响应式原理是根据Proxy来代理对象实现响应式,所以我们创建一个reactive函数, 返回值创建一个proxy,并且声明get与set函数
// reactive.ts
export function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
const res = Reflect.get(target, key)
return res
},
set(target, key, newValue) {
const res = Reflect.set(target, key, newValue)
return res
}
})
}
这时我们已经可以发现测试已经可以通过。
实现 isReactive
我们在测试中再添加一点新的API
// reactive.spec.ts
describe("reactive", () => {
it("happy path", () => {
const original = { foo: 1 }
const observer = reactive(original)
expect(original).not.toBe(observer)
expect(observer.foo).toBe(1)
expect(isReactive(observer)).toBe(true)
expect(isReactive(original)).toBe(false)
})
})
新添加isReative与isProxy这两个API,可以分析一下问题。这时候observer已经是一个Proxy对象,当访问observer时,会触发get函数。而我们的目标就是,给isReative传入一个值,来判断这个值是否是一个reactive,所以我们就可以利用这一特性来实现这个功能。
在reactive.ts中创建一个isReactive函数
export function isReactive(value) {
return !!value[ReactiveFlags.ISREACTIVE];
}
当value访问其中的属性时,如果是一个reactive对象,就会触发proxy的get函数,因此,我们声名一个枚举类型对象RactiveFlags
// reactive.ts
export const enum ReactiveFlags {
ISREACTIVE = "__v_reactive"
}
在对proxy对象的get函数添加一个判断,当key为__v_reactive时,就说明使用了isReactive函数,并返回true
// reactive.ts
export function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
if ( key === ReactiveFlags.ISREACTIVE){
return true
}
const res = Reflect.get(target, key)
return res
},
set(target, key, newValue) {
const res = Reflect.set(target, key, newValue)
return res
}
})
}
如过isReactive传入的value上没有__v_reactive这个属性,那isReactive就会返回一个undefined,那自然就不是一个reacitive,我们再使用!!运算符将结果转化为Boolean,这是测试已经可以全部通过
实现 isProxy
我们再添加一个新的API,isProxy
describe("reactive", () => {
it("happy path", () => {
const original = { foo: 1 }
const observer = reactive(original)
expect(original).not.toBe(observer)
expect(observer.foo).toBe(1)
expect(isReactive(observer)).toBe(true)
expect(isReactive(original)).toBe(false)
expect(isProxy(observer)).toBe(true)
})
})
我们可以利用isReactive来实现这个功能
// reactive.ts
export function isProxy(value) {
return isReactive(value)
}
这时测试已经全部通过
实现 effect 函数
reactivity模块的实现,最重要的就是依赖收集与触发,而effect则是实现这一功能的桥梁或者说是开关。
同样,我们先写出一个测试
// effect.spec.ts
describe("effect", () => {
it("happy path", ()=>{
const user = reactive({
age: 10
});
let nextAge;
effect(()=>{
nextAge = user.age + 1
})
expect(nextAge).toBe(11)
user.age++;
expect(nextAge).toBe(12)
})
})
期望effect函数可以接受一个函数,这个函数内部包括我们期望实现响应式的数据或依赖。当effect函数传入一个依赖时,我们就可以收集这个依赖,并且自动运行这个依赖。当其中的响应式数据发生改变时,我们可以监测到这个变化,并且及时更新依赖。 以上就是初步的一些关于effect函数的想法,下面我们来实现它。
我们先创建一个effect函数,并使用OOP(Object-oriented programming 面向对象编程)的思想,实例化一个ReactiveEffect对象
// effect.ts
export function effect(fn) {
const _effect = new ReactiveEffect(fn);
_effect.run();
}
ReativeEffect对象需要传入一个依赖函数,并且需要一个run函数来运行依赖函数。
// effect.ts
class RactiveEffect {
private _fn;
constructor(fn) {
this._fn = fn;
}
run() {
const result = this._fn();
return result
}
}
这是我们已经完成了运行依赖函数这一功能,那如何该收集这个依赖呢?对,我们可以利用前面封装的Proxy中的get与set方法来实现依赖收集与触发。
当响应式数据被访问到时,会触发get方法,这时候我们可以使用track函数将当前依赖收集储存在dep中。当该响应式对象被重新赋值时,会触发set方法,这时候我们可以使用trigger在收集起来的依赖dep中找到对应的依赖,来重新执行,这时我们就可以实现依赖的实时更新。
// reactive.ts
export function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
const res = Reflect.get(target, key)
track(target, key)
return res
},
set(target, key, newValue) {
const res = Reflect.set(target, key, newValue)
trigger(target, key, newValue)
return res
}
})
}
在get和set方法中添加track与trigger后,我们就可以实现对依赖的收集与触发。
// effect.ts
const targetMap = new Map();
export function track(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
depsMap = targetMap.get(target);
}
let deps = depsMap.get(key);
if (!deps) {
deps = new Set();
depsMap.set(key, deps);
}
effectTrack(deps);
}
function effectTrack(deps) {
deps.add(activeEffect);
}
export function trigger(target, key, value) {
const depsMap = targetMap.get(target);
const deps = depsMap.get(key);
effectTrigger(deps);
}
function effectTrigger(deps) {
for (let effect of deps) {
effect.run();
}
}
先创建一个targetMap来存放target与key的对应关系,在运行track时,先去targetMap中寻找是否有对应target的depsMap,如果没有,我们就进入初始化流程,创建一个depsMap,并将他与target对应存在targetMap中。之后,在根据key来取出对应的deps与就是依赖,如果没有依赖的化,就进入初始化流程。创建一个set来存放对应的deps。
容器有了,那依赖我们该如何获取呢?我们创建一个activeEffect来存放当前effet实例,这样我们就获取到了当前的依赖。
// effect.ts
let activeEffect;
class RactiveEffect {
private _fn;
constructor(fn) {
this._fn = fn;
}
run() {
activeEffect = this;
const result = this._fn();
return result
}
}
之后我们就可以将activEffect存入到deps中,就获取到了当前的依赖。他们的对应关系如下
这时我们的测试已经可以全部跑通,已经初步实现了reactive与effect的依赖收集与触发功能
以上就是Reactive与effect的初步实现。在这一系列中,我将使用TDD的思想来一步步完善Reactivity模块。
这是我的github仓库mini-vue3,如果这对你有帮助就给我一个star吧。