Reactivity 核心
前言
Vue3 最核心的就是响应式机制的变化,那么和 Vue2 响应式的区别是什么?
- 颗粒度更细致,增加静态节点、事件侦听缓存机制
- diff 算法就不会去 patch
- 使用 proxy 来替代 Object.defineProperty()
- 两者的优劣对比
- 例如 proxy 多达 13 种拦截方式,且可以对对象进行逐一监控。而 Object.defineProperty() 是对整个对象。等等
图解
本文阅读时间大约为12分钟,请听笔者娓娓道来。
实现步骤
Vue3 响应式就是通过 get(取值操作) 的时候收集依赖,set(赋值操作) 的时候触发依赖。
咱们目前只实现核心部分,不涉及边缘 case
创建 proxy 对数据进行代理
需要实现的单测
// effect.spec.ts
it('effect', () => {
// reactive 核心:get 收集依赖 set 触发依赖
const user = reactive({
age: 10
})
});
export function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
const res = Reflect.get(target, key)
// Proxy 要和 Reflect 配合使用
// Reflect.get 中 receiver 参数,保留了对正确引用 this(即 admin)的引用,该引用将 Reflect.get 中正确的对象使用传递给 get
// 不管 Proxy 怎么修改默认行为,你总可以在 Reflect 上获取默认行为
track(target, key)
return res
},
set(target, key, value) {
// set 操作是会放回 true or false
// set() 方法应当返回一个布尔值。
// 返回 true 代表属性设置成功。
// 在严格模式下,如果 set() 方法返回 false,那么会抛出一个 TypeError 异常。
const res = Reflect.set(target, key, value)
trigger(target, key)
return res
}
})
}
收集依赖
Vue3 是通过 effect 函数来收集与响应式变量相关的 fn。
需要实现的单测
describe('test effect', () => {
it('effect', () => {
const user = reactive({
age: 10
})
let nextAge
effect(() => {
nextAge = user.age + 1
})
expect(nextAge).toBe(11)
});
});
实现 effect 函数,并将传入的函数执行。
export class ActiveEffect {
private _fn: any
constructor(fn) {
this._fn = fn
}
run() {
this._fn()
}
}
export function effect(fn) {
// 通过构造 ActiveEffect 来提供多种属性和方法
const _effect = new ActiveEffect(fn)
_effect.run()
}
当函数执行nextAge = user.age + 1
的时候,是要先 get 到 user.age 的值,然后再 set 给 nextAge。
export function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
// 拿到 user.age 的值
const res = Reflect.get(target, key)
// 收集依赖
track(target, key)
return res
},
})
}
实现 track,Vue3 是通过 Map 和 Set 对 fn 和 变量 进行收集处理。Map 的好处就是,键可以是对象,而 Set 能保证 fn 唯一。
举个🌰
// 例如你定义了这样一个变量
const foo = reactive({num:1,age:18})
// 在 Vue3 中是这样保存的
targetMap={
{num:1,age:18}:{
num:(fn1,fn2),
age:(fn1,fn2)
}
}
// 如果有多个,如下
targetMap(weakMap) = {
target1(Map): {
key1(dep_Set): (fn1,fn2,fn3...)
key2(dep_Set): (fn1,fn2,fn3...)
},
target2(Map): {
key1(dep_Set): (fn1,fn2,fn3...)
key2(dep_Set): (fn1,fn2,fn3...)
},
}
// 定义全局变量 targetsMap
// WeakSet 的好处就是 WeakSet 中的对象都是弱引用
const targetsMap = new WeakSet()
// 收集依赖
export function track(target, key) {
// reactive 传入的是一个对象 {}
// 收集关系: targetsMap 收集所有依赖 然后 每一个 {} 作为一个 depsMap
// 再把 {} 里面的每一个变量作为 dep(set 结构) 的 key 存放所有的 fn
let depsMap = targetsMap.get(target)
// 不存在的时候 要先初始化
if (!depsMap) {
depsMap = new Map()
targetsMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
// 要存入的是一个 fn,这个 fn 是 effect 中传入并执行的函数
// 所以要利用一个全局变量
dep.add(activeEffect)
}
let activeEffect
export class ActiveEffect {
...
run() {
// === 新增 ===
activeEffect = this
}
}
触发依赖
刚刚咱们实现了触发 get 的时候收集依赖,现在来实现触发 set 的时候触发依赖。
需要实现的单测
describe('test effect', () => {
it('effect', () => {
// update
// user.age++ => user.age = use.age + 1
user.age++
expect(nextAge).toBe(12)
});
});
export function reactive(raw) {
return new Proxy(raw, {
...
set(target, key, value) {
const res = Reflect.set(target, key, value)
trigger(target, key)
return res
}
})
}
// 触发依赖
// 我们只需要通过相应的 dep,然后遍历执行 fn,所有的依赖的值就会改变
export function trigger(target, key) {
let depsMap = targetsMap.get(target)
let dep = depsMap.get(key)
for (const effect of dep) {
effect.run()
}
}
总结
通过 TDD 单测驱动学习,更有效的让你了解运行的机制。Vue3 的源码内也提供了大量的测试。本节,咱们大概的讲述了收集依赖和触发依赖的过程,本节调试代码链接在文章开头。学习源码一定要多思考,为什么会这么做?这样做有什么好处?等等。最后,推荐大家,学习 Vue3 源码时,可以跟着 mini-vue3 作者一起敲。有想加群的小伙伴,评论区联系我呀!(没人带,举步维艰!有人带,事半功倍!)