实现effect & reactive & 依赖收集 & 依赖触发
**开始前请先了解响应式篇梗概:响应式篇总结
最近在学习vue3源码,越发感觉尤大牛逼,所以将掘金的处女作献给vue3源码。之前也有写一些东西,不过是在自己的博客上写写东西,当笔记记录下方便查看。由于自己表达能力较差,一开始是拒绝在掘金上发文章的,但是因为欠缺,所以才更要锻炼。文章我会尽量写的细一些,希望能帮到对源码感兴趣的同学。
下面内容都是按照以下流程做的,先根据测试用例和功能描述了解每个模块是做什么的,怎么用,然后再去看它的实现代码,实现完成测试通过后再去优化它。没有将文字描述写在外面,而是通过注释写在了代码块中,觉着看每一行代码的时候可以保持思路清晰,也是我个人的一些小习惯。
- 测试用例
- 功能描述
- 具体实现
- 优化
文章中的测试用例执行需要配置环境:vsCode构建基于ts及jest的测试环境,使用webStrom的话只需要安装相关的依赖包即可。
抽象概念:响应式对象中包含一个容器,这个容器需要收集这个对象所有的依赖(通过effect收集)。
reactive
// 测试用例
describe('reactive', () => {
it('happy path', () => {
const original = { foo: 1 } // 原始对象
const observed = reactive(original) // 创建代理对象
expect(observed).not.toBe(original) // 代理对象不等于原始对象
expect(observed.foo).toBe(1) // 代理对象的foo属性等于1
})
})
创建代理对象
const proxyInstance = new Proxy(target, handlers)
创建代理对象的核心是处理器,关键的逻辑就在处理器中
先实现处理器的 get 和 set
- get
- 返回代理对象中key对应的值
- 收集依赖
- set
设置代理对象中key对应的值
触发依赖
实现思路
创建代理对象并将其返回,核心在于收集和触发依赖
// 实现reactive
function reactive (raw) {
return new Proxy(
raw,
{
get (target, key) {
// todo 收集依赖(待实现)
track(target, key)
return Reflect.get(target, key)
},
set (target, key, value) {
// todo 触发依赖(待实现)
trigger(target, key)
return Reflect.set(target, key, value)
}
}
)
}
effect
effect接受一个函数 fn,每次开始会调用 fn,调用 fn 时访问user.age,
触发了user对象的get方法,当触发get操作的时候,user对象就可以收集到这个fn,这个流程就是依赖收集。
当修改对象的时候,会触发代理对象的set方法,在set中会将所有的依赖调用一次,这个流程就是触发依赖。
// 测试用例
describe('effect', () => {
it('happy path', () => {
// 1.user是一个响应式对象,响应式对象包含一个容器,需要收集它所有的依赖
const user = reactive({ age: 10 })
let nextAge
// 2.通过effect收集依赖,传入一个cb并调用它
effect(() => {
// 3.调用cb会调用user这个响应式对象的get方法,触发get的时候
// user响应式对象就可以把这个依赖收集起来
nextAge = user.age + 1
})
expect(nextAge).toBe(11)
// 更新触发依赖
// 4.当触发set操作的时候,会把所有收集的依赖调用一遍
user.age++
expect(nextAge).toBe(12)
})
})
// 实现effect - 首次执行
// 抽象出一个类,保存fn,run方法执行fn
class ReactiveEffect {
private _fn: any
constructor (fn) {
this._fn = fn
}
run () {
this._fn()
}
}
// 调用effect的时候,创建一个对应的reactiveEffect实例,然后调用它的run方法完成首次执行
function effect (fn) {
const _effect = new ReactiveEffect(fn)
effect.run()
}
track - 收集依赖
// 收集依赖
const targetMap = new Map() // 所有对象的依赖保存在这个全局变量中
let activeEffect // 需要将传入effct的fn保存起来,利用一个全局变量来获取它
export function track (target, key) {
// 拿到这个对象对应的依赖项
let depsMap = targetMap.get(target)
if (!depsMap) { // 判空
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 拿到这个key对应的依赖项(dep中保存的是所有activeEffect实例而不是fn)
let dep = depsMap.get(key)
if (!dep) { // 判空
// 因为要保证每个传入effect的fn都是唯一的,所以使用set数据结构
dep = new Set()
depsMap.set(key, dep)
}
// 将这个key对应的依赖添加到dep中
dep.add(activeEffect)
}
// 对run方法进行优化
run () {
/*
将activeEffect赋值为activeEffect实例
run方法正在执行的时候将activeEffect实例赋值给activeEffect,然后会在fn执行的时候触发响应式对象的get方法(get方法中包含track),于是通过dep.add(activeEffect)将依赖项添加到了dep中
*/
activeEffect = this
this._fn()
}
trigget - 触发依赖
function trigger (target, key) {
// 基于target和key获取到dep(这个key对应的所有依赖项的集合),然后全部执行
const depsMap = targetMap.get(target)
const dep = depsMap.get(key)
for (const effect of dep) {
effect.run()
}
}
targetMap存在的问题
现在的targetMap是用的Map数据结构,存在一个问题:
当targetMap引用的对象被清空时,这个对象在Map中的引用仍然存在,可能会导致内存泄漏。源码中使用的weakMap,它的键只能是对象,而且是弱引用,当引用的对象被清空时,weakMap中对应的key-value会被清空。详情见:map与weakMap