reactivity库的实现----reactive
测试文件
测试文件可以更好的帮我们检测实现的功能是否正确,而且更加有助于我们重构和优化代码,所以测试文件是非常必要的。
import { reactive } from "../reactive";
describe("reactive", () => {
it("happy path", () => {
const original = { foo: 1 };
const observed = reactive(original);
expect(observed).not.toBe(original);
expect(observed.foo).toBe(1);
});
});
首先我们应该实现测试文件,引入我们自己实现的reactive模块(可能这时还没有实现,那就先放着)。测试文件中,定义了一个original,并且通过调用reactive返回一个响应式对象observed,此时我们希望得到的结果是observed不等于original,希望observed.foo等于1。这些都是我们希望得到的结果 ,下面来实现一下这个简单的功能。
reactive的实现
reactive get
export function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
const res = Reflect.get(target, key)
return res
}
})
}
我们都知道vue3实现响应式是用proxy,测试文件中值涉及到了数据的读,所以实现get功能就可以了,此时这个测试文件就可以通过了。
effect
此时我们想要去实现set的功能,那么就需要考虑到依赖的收集和触发,可以集合effect来实现。下面我们看下也给测试用例。
import { effect } from "../effect"
import { reactive } from "../reactive"
describe('effect', () => {
it('happy path', () => {
const user = reactive({
age: 10
})
let nextAge
effect(() => {
nextAge = user.age + 1
})
expect(nextAge).toBe(11)
})
})
这个测试用例主要想实现的功能很简单,effect这个函数,传一个函数,而这个函数被立即调用,所以定义的变量nextAge等于11。而这仅仅是我们希望得到的结果。下面看下怎么实现。
class ReactiveEffect {
private _fn: any;
constructor(fn) {
this._fn = fn
}
run() {
this._fn()
}
}
export function effect(fn) {
const _effect = new ReactiveEffect(fn)
_effect.run()
}
首先定义effect函数,基于面向对象的思想,我们抽离出一个类ReactiveEffect,这个类接收fn,并定义一个run方法去执行fn。这种实现方式一目了然,我们的测试用例就可以通过了。
此时我们只是实现了effect第一层,下面我们想实现的是,当fn中的响应式数据发生变化的时候,怎么去通知fn再次执行呢?下面我们添加一下测试用例。
import { effect } from "../effect"
import { reactive } from "../reactive"
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)
})
})
此时我们的测试用例加了一步,当user.age++期望nextAge的值为12。如何让这个成立呢?不就是让这个fn再次执行一下吗?所以我们这里就要考虑到响应式数据的依赖和收集。
reactive 依赖收集
user.age是一个响应式数据,当执行effect执行的时候,effect的参数fn会立即执行,这是会访问到我们的响应式数据age,那么就会触发get,这时候就应该去把age这个响应式对应的依赖收集起来。
import { track } from "./effect"
export function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
const res = Reflect.get(target, key)
track(target, key)
return res
}
})
}
对于依赖的收集和触发统一放在effect文件中操作
let activeEffect
class ReactiveEffect {
···
run() {
activeEffect = this
this._fn()
}
}
const targetMap = new Map()
export function track(target, key) {
// 映射关系
// target -> key -> dep
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
dep.add(activeEffect)
}
回到effect文件中,我们添加一个函数track,和一个变量activeEffect。activeEffect是一个全局的变量,在执行run方法时被赋值this,这个就是我们要收集的依赖。在track中,我们把依赖收集到Map中。大致的对应关系应该是这样的。
reactive set
当响应式数据发生变化的时候,那么就就会触发依赖更新。当user.age++的时候,会触发set,并且执行trigger方法去触发依赖的更新。
import { track,trigger } from "./effect"
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, val) {
const res = Reflect.set(target, key, val)
trigger(target, key)
return res
}
})
}
reactive 触发依赖更新
下面看下trigger的实现
let activeEffect
class ReactiveEffect {
···
}
const targetMap = new Map()
export function track(target, key) {
···
}
export function trigger(target, key) {
let depsMap = targetMap.get(target)
let dep = depsMap.get(key)
for (const effect of dep) {
effect.run()
}
}
export function effect(fn) {
···
}
当执行trigger的时候,会通过参数target、key在Map中找到对应的依赖dep,dep是一个数组,因为我们一个响应式数据对应的可能不止是一个依赖,所以我们要把相关的所有的依赖收集到dep,当我们需要触发依赖的时候,只需要去遍历dep,拿到每一个依赖,也就是之前收集的activeEffect,并执行run就OK了。此时测试用例就通过了。
我们简易版的reactive就完成了。大家可以动手试一试哦。后续还会有更多的文章输出。
欢迎大家加入崔学社
如果大家想去了解更多的相关知识,我推荐一位大佬,大家可以加他wx:cuixr1314,进入崔学社一起学习哈。这位大佬自己一人实现了一个比较全面的mini-vue,对vue3的了解还是比较全面的,而且有mini-vue的课程哦。