在 Vue3 中, 通过 Proxy 对象来实现对一个对象属性 的访问和设置,从而达到依赖收集和触发的功能
示例
这是一个 Proxy 的使用示例:
const target = { a: 1 }
const proxy = new Proxy(target, {
get(target, key, receiver) {
// target: 源对象
// key: 访问的属性名称
// receiver:代理对象,即 proxy
if (key in target) {
return target[key]
}
return -1
},
set(target, key, value, receiver) {
// target: 源对象
// key: 访问的属性名称
// value 设置的属性值
// receiver:代理对象,即 proxy
target[key] = value
}
})
proxy.a // 1
proxy.b // -1
proxy.c // -1
proxy.b = 2
proxy.b // 2
proxy.c // -1
可以看到,使用 Proxy 对象生成的代理对象可以检测到对源对象的任意属性值访问,即使是不存在的属性,这与 Vue2 中使用的 Object.defineProperty
是不同的,因此 Vue2 中在初始化数据时,需要对对象进行遍历来重新定义属性,Vue3 中不需要,这是一个性能提升点
了解了这个对象的使用方法后,可以很快的写出 Vue3 中的核心 API:effect
和 reactive
:
初步实现一个响应式对象
// 存储当前正在执行的的 ReactiveEffect
let activeEffect: ReactiveEffect | undefined = undefined
// 使用 “对象 => 对象的 key => Set<ReactiveEffect>” 结构来存储对象的key对应的多个 ReactiveEffect
type KeyToDepMap = Map<any, Set<ReactiveEffect>>
const targetMap = new WeakMap<any, KeyToDepMap>()
class ReactiveEffect {
constructor(public fn: () => any) {}
run() {
try {
activeEffect = this
return this.fn()
} finally {
activeEffect = undefined
}
}
}
function reactive<T extends object>(object: T) {
return new Proxy(object, {
get(target, key, receiver) {
// 在这里进行依赖的收集
if (activeEffect) {
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)
}
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
// 记得先设置值,再触发副作用,这样副作用中才能访问到最细的值
const res = Reflect.set(target, key, value, receiver)
// 在这里就行触发收集到的副作用
const depsMap = targetMap.get(target)
if (depsMap) {
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect.run())
}
}
return res
}
})
}
// effect 是一个在文档没有介绍的 API
function effect(fn: () => any) {
const _effect = new ReactiveEffect(fn)
// effect 会立即运行一次,在这个时候来收集依赖
_effect.run()
}
// 使用示例
const state = reactive({ a: 1 })
effect(() => {
console.log(state.a)
})
state.a = 2
运行这一段代码,会有两次输出,分别是 1 和 2,第一次输出原因为 effect 立即执行的一次回调,也是在这个时候进行了依赖收集的工作,第二次输出是在给属性 a 重新设置值时,此时触发了代理对象的 setter,从而找出收集到的 ReactiveEffects 执行
在 getter 和 setter 中,使用了 Reflect 对象,Reflect 和 Proxy 一样也是 JavaScript 提供的原生对象,Reflect 功能是可以在取值或设置值时候,修改属性访问器等中的 this 指向,看下面一个案例:
const target = {
name: 'zhangsan',
get alias() {
return this.name
}
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
target[key] // this 指向 target,即源对象
Reflect.get(target, key, receiver) // this 指向 receiver ,即 proxy
}
})
// 通过代理对象访问 alias 属性
proxy.alias
因此使用 Reflect 来读取对象属性、设置对象属性值可以正确的追踪到每一个依赖,因为通过 target[key] 是不经过代理对象的,不经过代理对象就不会触发 getter,就不会被收集
嵌套的 effect
在实际的开发中,effect 是可以嵌套使用的,在 Vue3 中,有以下写法:
const state = reactive({ a: 1, b: 2, c: 3 })
effect(() => {
console.log(state.a)
effect(() => {
console.log(state.b)
})
console.log(state.c)
})
外层 effect 会收集到属性的 a、c 作为依赖, 内层的 effect 会收集到属性 b 作为依赖,在属性 a/c 值变化时,外层 effect 会重新执行,当属性 b 值变化时,内层的 effect 重新执行
回想上面我们拿到当前正在执行的 ReactiveEffect 的地方(可以看下面代码),按照嵌套 effect 来分析我们的代码:
- 运行外层 effect 时,activeEffect = effect外,effect外 收集到属性 a
- 在执行过程中,遇到了内层 effect,即会将 activeEffect 设置为 effect内,effect内 收集到属性 b
- effect内 执行完毕时,将 activeEffect 设置为了 undefined
- 回到外层执行
console.log(state.c)
时,由于 activeEffect 为 undefined,即在我们的代码中,effect外 收集不到属性 c 作为依赖
class ReactiveEffect {
constructor(public fn: () => any) {}
run() {
try {
activeEffect = this
return this.fn()
} finally {
activeEffect = undefined
}
}
}
解决办法也肯简单,很容易想到我们可以模拟栈结构来存储正在执行的 effect,当一个内部 effect 执行完成后,弹栈再取外层的 effect,如下面这个实现(部分代码):
let activeEffects: ReactiveEffect[] = []
class ReactiveEffect {
constructor(public fn: () => any) {}
run() {
try {
activeEffects.push(this)
return this.fn()
} finally {
activeEffects.pop()
}
}
}
// 在 getter 中
const proxy = new Proxy({}, {
get(target, key, receiver) {
if (activeEffects.length > 0) {
// ...
dep.add(activeEffects[activeEffects.length - 1])
}
}
})
在 Vue3 中,通过在 ReactiveEffect 对象中记录 parent 属性来解决,以下是 Vue3 的实现方式:
let activeEffect: ReactiveEffect | undefined = undefined
class ReactiveEffect {
parent: ReactiveEffect | undefined
constructor(public fn: () => any) {}
run() {
try {
this.parent = activeEffect
activeEffect = this
return this.fn()
} finally {
activeEffect = this.parent
this.parent = undefined
}
}
}
// 在 getter 中使用方式同最开始的判断,不变
分支依赖处理
有以下代码:
const state = reactive({ flag: true, a: 1, b: 2 })
effect(() => {
console.log(state.flag ? state.a : state.b)
})
state.flag = false
state.a = 10
在上述实现的代码中:
- effect 首次运行回调时,收集到了属性 flag 和 属性 a 作为依赖
- 在更新了 flag 值之后,effect 重新执行,又收集到了属性 b 作为依赖,此时 effect 收集到了 flag、a、b 三个依赖
- 更新属性 a 的值,会重新执行回调,但事实上是不需要重新执行的,因为这个时候 effect 因 flag 的值是 false,根本不会使用到属性 a 的值
根据前面逻辑,在触发 setter 时,会找到这个属性名对应 Set<ReactiveEffect>
,依次去执行里边的每个 ReactiveEffect,为了解决上述问题,我们需要在执行 ReactiveEffect 的回调之前(每个),清除掉当前执行的 ReactiveEffect 与触发属性的 key 的对应关系
在 ReactiveEffect 上新增一个属性 deps,用于收集所有包含自己的属性对应的依赖集合,即所有包含自己的 Set<ReactiveEffect>
+ function cleanupEffect(effect: ReactiveEffect) {
+ const { deps } = effect
+ deps.forEach(dep => {
+ dep.delete(effect)
+ })
+ deps.length = 0
+ }
class ReactiveEffect {
parent: ReactiveEffect | undefined
+ deps: Set<ReactiveEffect>[] = []
constructor(public fn: () => any) {}
run() {
try {
this.parent = activeEffect
activeEffect = this
cleanupEffect(this)
return this.fn()
} finally {
activeEffect = this.parent
this.parent = undefined
}
}
}
function reactive<T extends object>(object: T) {
return new Proxy(object, {
get(target, key, receiver) {
if (activeEffect) {
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)
+ activeEffect.deps.push(dep)
}
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
// ...
}
})
}