前言
注:本文过长过干,观看前请备足水分。and,虽然是ts写的,但是并不关心类型,所有的类型注解基本只是为了防报错。
在手写Vue3核心代码之前,作者完全不知道TDD(测试驱动开发)是啥,在完成下文代码之前补习了一下TDD的基本思想以及一款测试框架——jest。所以在阅读之前,最好希望你对jest框架有基本的认知(阅读官方文档半小时能入门),以及明白为何在完成功能之前需要先写测试代码——这有助于减少调试的耗时,细致拆分的测试文件能够将bug扼杀在摇篮之中。
同时,不仅仅是完成代码,为了保证代码的可读性还会对完成功能后的代码进行重构,当然作者的重构能力仅限参考,清谨慎阅读😀。
reactivity的核心流程
以一段reactive和effect的配合使用的代码来说明响应式的核心流程——依赖收集以及依赖触发。
const user = reactive({
age: 10
})
let nextAge
effect(() => {
nextAge = user.age + 1
})
console.log(nextAge) //11
//update
user.age++
console.log(nextAge) //12
这里演示了一个最基本功能的数据响应式案例,effect函数和reactive函数都是reactivity模块中的核心API,reactive函数通过Proxy代理传入的对象,在getter和setter阶段会分别执行依赖收集以及依赖的触发的工作。而effect函数接收一个回调函数,初始默认执行一次,并在回调中对应的依赖更新时再次执行。
当然reactivity还包含其他响应式API如ref等,这里先实现最基本的reactive和effect模块。
reactive和effect的实现
分别测试reactive和effect的逻辑,单元测试(jest) 代码如下:
//effect.spec.ts
//effect测试了reactive的响应式
describe('effect', () => {
it('happy path', () => {
const user = reactive({
age: 10
})
let nextAge
effect(() => {
nextAge = user.age + 1
})
expect(nextAge).toBe(11)
//update
user.age++
expect(nextAge).toBe(12)})
})
reactive和effect的代理和依赖收集及触发功能的实现
第一步,实现reactive的代理中的get和set
//reactive.ts
export function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
//收集依赖 在effect中实现track,因为要获取activeEffect对象
//...
return Reflect.get(target, key)
},
set(target, key, value) {
//触发依赖 在effect中实现trigger
//...
return Reflect.set(target, key, value)
}})
}
在这里拆分并测试一下reactive函数的逻辑,是可以通过的
//reactive.spec.ts
//测试reactive的代理功能
describe('reactive', () => {
it('happy path', () => {
const obj = { a: 1 }
const reactiveObj = reactive(obj)
expect(reactiveObj).not.toBe(obj)
expect(reactiveObj.a).toBe(1)}));
第二步,再get和set中分别收集依赖(track)和收集依赖(trigger),这两个函数的实现放在了effect.ts
文件中,因为需要利用外部作用域和effect函数共用activeEffect对象(当前活跃的ReactiveEffect对象),这个点后面就知道为什么了。
//effect.ts中的track和trigger的实现
let activeEffect: ReactiveEffect
const targetsMap = new Map
export function track(target, key) {
//读取targetsMap中target对应的depsMap
if (!targetsMap.has(target)) targetsMap.set(target, new Map())
let depsMap = targetsMap.get(target)
if (!depsMap.has(key)) depsMap.set(key, new Set)
let deps = depsMap.get(key)
//将现在的effect对象放入deps
deps.add(activeEffect)
}
export function trigger(target, key, value) {
//读取targetsMap中target对应的depsMap
if (!targetsMap.has(target)) targetsMap.set(target, new Map())
let depsMap = targetsMap.get(target)
if (!depsMap.has(key)) depsMap.set(key, new Set)
let deps = depsMap.get(key)
//通知依赖
for (let dep of deps) {
dep.run()}
}
第三步,编写effect函数的功能——执行传入的函数,并且能够配合track和trigger实现依赖的触发与收集
class ReactiveEffect {
fn: Function
constructor(fn) {
this.fn = fn}
run() {
this.fn()}
}
export function effect(fn: Function) {
//构造一个ReactiveEffect实例
let _effect = new ReactiveEffect(fn)
activeEffect = _effect
_effect.run()
}
这里不是直接在effect函数中执行fn,而是定义了一个ReactiveEffect对象,这个对象其实是观察者模式中的观察者(Observer),观察者上面定义了run函数,在主体(Subjcet)——也就是之前reactive所代理对象的某个key值发生改变时,会通知所有对应的观察者——即track和trigger中的deps集合中所包含的对象执行其run函数。
完善effect的功能
effect返回runner
effect的返回值runner是一个函数,且具有和传入的函数相同的功能和返回值。测试代码如下:
//测试返回runner的功能
it('return runner', () => {
let foo = 1
let fn = effect(() => {
foo++
return 'foo'
})
expect(foo).toBe(2)
const n = fn()
expect(foo).toBe(3)
expect(n).toBe('foo')});
})
实现也很简单,修改后的代码如下:
class ReactiveEffect {
private fn: Function
constructor(fn) {
this.fn = fn}
run() {
activeEffect = this
return this.fn()}
}
export function effect(fn: Function) {
//构造一个ReactiveEffect实例
let _effect = new ReactiveEffect(fn)
_effect.run()
//bind是因为run函数内部有this值的指向问题
return _effect.run.bind(_effect)
}
scheduler
effect的第二个参数是可选参数——配置对象options,其中的scheduler是一个函数对象,如果有scheduler的话,effect只会在初始化时执行fn,在之后的触发更新阶段都会执行scheduler。测试代码如下:
//测试scheduler功能
it('scheduler ', () => {
let run: any
let scheduler = function () {
run = runner
}
let foo = reactive({ bar: 1 })
let bee
let runner = effect(() => {
bee = foo.bar + 1
return 'runner'
}, {
scheduler: scheduler
})
expect(bee).toBe(2)
//trigger,执行的不是fn,而是scheduler
foo.bar++//foo.bar == 2 ,没有执行fn,所以bee还是2 ,执行了scheduler,所以run可以使用
expect(bee).toBe(2)
const n = run()//执行run,即执行fn函数,此时bee == foo.bar +1 ==3,且n为fn的返回值
expect(bee).toBe(3)
expect(n).toBe('runner')});
实现如下,只需要改变effect的二个参数为可选参数,以及改写ReactiveEffect对象的构造函数并向外暴露scheduler,在trigger中判断并调用即可。
//effect第二个参数为可选参数
export function effect(fn: Function, options?: any) {
//构造一个ReactiveEffect实例
let _effect = new Re