effect & reactive & 依赖收集 & 触发依赖
TIP
本篇笔记对应的分支号为: main分支:e8bb112
在 Vue3 中,reactive 方法被用于创建一个对象的 响应式副本。这里可以拆成两个部分来理解,即 响应式 以及 副本。
副本
我们先来看看 副本 这个部分。在实现 reactive 方法之前,我们先来写下它的测试用例,看看它需要做些啥:
// src/reactivity/__tests__/reactive.spec.ts
describe('reactive', () => {
it('happy path', () => {
const origin = { num: 0 }
// 通过 reactive 创建响应式对象
const reactiveData = reactive(origin)
// 判断响应式对象与原对象不是同一个对象
expect(reactiveData).not.toBe(origin)
// 代理对象中的 num 值应与原对象中的相同
expect(reactiveData.num).toBe(0)
})
})
实现 reactive
通过测试用例我们不难发现,其实 reactive 做的事情很简单,就是创建一个对象副本,那这个 副本 该怎么创建呢?答案是使用 Proxy 👇
// src/reactivity/reactive.ts
export const reactive = (raw) => {
return new Proxy(raw, {
// 取值
get(target, key) {
const res = Reflect.get(target, key)
return res
},
// 赋值
set(target, key, value) {
const res = Reflect.set(target, key, value)
return res
}
})
}
响应式
现在我们已经可以通过 reactive 方法获取目标对象的 副本 了,那 响应式 部分又该如何实现呢?
所谓 响应式, 其实本质上就做了两件事情:
- 在读取对象属性时进行
依赖收集- 在修改对象属性时执行
依赖触发
而这部分的逻辑则交由 effect 模块来实现。那 依赖收集 跟 依赖触发 具体是怎样的一个流程呢?请看下图:

对上图的内容简单描述如下:
- 在读取响应式对象
Target中的属性时进行依赖收集操作,所有的依赖会被收集到依赖池TargetMap中;- 在设置响应式对象
Target的属性值时执行依赖触发操作,会根据对应的Target以及key将依赖从依赖池TargetMap中取出并执行。
现在我们已经知道了 effect 模块所要实现的功能,依据上述内容,先来编写下测试用例:
// src/reactivity/__tests__/effect.spec.ts
describe('effect', () => {
it('happy path', () => {
// 创建响应式对象
const user = reactive({
age: 10
})
let nextAge
effect(() => {
nextAge = user.age + 1
})
// 传入 effect 的方法会被立即执行一次
expect(nextAge).toBe(11)
// 修改响应式对象的属性值
user.age++
// 传入 effect 的方法会再次被执行
expect(nextAge).toBe(12)
})
})
实现 effect
接下来我们需要实现 effect 模块的功能。
根据上面的描述,effect 接受一个函数作为参数,既如此先定义一下 effect 方法:
// src/reactivity/effect.ts
export function effect(fn) {}
接下来,我们需要定义依赖池 targetMap 用于存放依赖。依赖池中存放的是响应式对象 target 所对应的依赖,需要使用对象类型作 key 的话,那么使用 Map 自然再合适不过啦:
// src/reactivity/effect.ts
const targetMap = new Map()
export function effect(fn) {}
好了,现在存放依赖的地方有了,那么我们就开始收集它们吧~
上文中我们提到,收集依赖 的操作是在读取响应式对象 target 中的属性时进行的。还记得 target 对象是通过 Proxy 创建出来的么?在读取 target 的属性时,必然会触发 get 方法,那么 收集依赖 的操作也应该在 get 方法中进行。
我们先来定义一个方法 tarck 用于依赖收集,并在 reactive.ts 中引入它,以便在 get 方法中进行调用:
// src/reactivity/effect.ts
const targetMap = new Map()
/**
* 收集依赖
* @param target 需要收集依赖的对象
* @param key 收集该key所对应的依赖
*/
export function track(target, key) {
}
export function effect(fn) {}
// src/reactivity/reactive.ts
import { track } from './effect'
export const reactive = (raw) => {
return new Proxy(raw, {
// 取值
get(target, key) {
const res = Reflect.get(target, key)
// 收集依赖
track(target, key)
return res
},
// 赋值
set(target, key, value) {
const res = Reflect.set(target, key, value)
return res
}
})
}
接下来,我们需要实现 track 这部分的功能。在动手实现之前,我们先来捋一捋 track 需要做哪些事情:
- 由于在初始化时依赖池是空的(也为了避免覆盖),所以在存入
targetMap依赖池之前,需要先判断依赖池中是否已经存在target所对应的依赖容器depsMap:
- 如果存在,则取出
depsMap;- 否则新建一个
depsMap, 并将其存入到依赖池targetMap中;- 从依赖容器
depsMap中取出响应式对象target对应属性的依赖deps,由步骤1可知,depsMap可能是空的,因此也需要对deps进行判空处理:
- 如果存在,则取出,并将依赖存入
- 如果不存在,则新建一个
deps,将依赖存入其中,并将deps存入对应属性的依赖容器depsMap中。为了避免重复收集依赖,此处使用 Set 进行存储。
为了方便理解,我们来一起看下流程图:

代码实现如下:
// src/reactivity/effect.ts
const targetMap = new Map()
/**
* 收集依赖
* @param target 需要收集依赖的对象
* @param key 收集该key所对应的依赖
*/
export function track(target, key) {
// 查找该对象对应的依赖池
let depsMap = targetMap.get(target)
// 如果没有(首次初始化时),则创建新的依赖池
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 从获取到的依赖池中获取该key所对应的依赖列表
let deps = depsMap.get(key)
// 如果没有,则新建一个该key对应的列表
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
// TODO 将依赖对象保存到列表中
}
export function effect(fn) {}
好,代码写到这里的时候,我们遇到了一个
问题:
需要被收集的依赖在 effect 方法中,在 tarck 里要怎么获取到这个依赖呢?
针对这个问题,我们可以通过定义一个用于存储依赖的全局变量 activeEffect 来解决解决这个问题。那我们直接把依赖塞到 activeEffect 中就完事儿了么?当然。。。。

不是!如果只单单为了实现这个功能,无可厚非,但是后续我们还有其他操作(为了代码的健壮性,可读性, 可扩展性),这里我们定义 ReactiveEffect 类将依赖收集起来,之后将该类的实例赋值给 activeEffect 即可:
// src/reactivity/effect.ts
let activeEffect
class ReactiveEffect {
private _fn: any
constructor(fn) {
this._fn = fn
}
run() {
activeEffect = this
this._fn()
}
}
const targetMap = new Map()
/**
* 收集依赖
* @param target 需要收集依赖的对象
* @param key 收集该key所对应的依赖
*/
export function track(target, key) {
// 查找该对象对应的依赖池
let depsMap = targetMap.get(target)
// 如果没有(首次初始化时),则创建新的依赖池
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 从获取到的依赖池中获取该key所对应的依赖列表
let deps = depsMap.get(key)
// 如果没有,则新建一个该key对应的列表
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
// 将依赖对象保存到列表中
deps.add(activeEffect)
}
export function effect(fn) {
// 实例化 ReactiveEffect 类,并将依赖传入
const _effect = new ReactiveEffect(fn)
_effect.run()
}
注意
这里需要注意的是,传入 effect 中的方法会被立即执行一次(可以回看上述测试用例中的 第14行代码)。所以 ReactiveEffect 暴露的 run 方法中除了要将依赖存入全局变量 activeEffect 中,还得将传入的依赖返回出来用以执行。
到目前为止,依赖收集 的功能就已经实现了。接下来便轮到 依赖触发 了。相较于 依赖收集,依赖触发 就简单了,只需要根据传入的 target 以及对应的属性 key,将依赖项取出执行便可。
这里我们在 effect.ts 中定义一个 trigger 方法用于触发依赖,之后在 reactive.ts 中引入。由于触发依赖发生在修改响应式对象 target 的属性阶段,所以需要放到 set 中执行:
// src/reactivity/effect.ts
/**
* 触发依赖
* @param target 触发依赖的对象
* @param key 触发该key对应的依赖
*/
export function trigger(target, key) {
// 根据对象与key获取到所有的依赖,并执行
const depsMap = targetMap.get(target)
const deps = depsMap.get(key)
for(const dep of deps) {
dep.run()
}
}
// src/reactivity/reactive.ts
import { track, trigger } from './effect'
export const reactive = (raw) => {
return new Proxy(raw, {
// 取值
get(target, key) {
const res = Reflect.get(target, key)
// 收集依赖
track(target, key)
return res
},
// 赋值
set(target, key, value) {
const res = Reflect.set(target, key, value)
// 触发依赖
trigger(target, key)
return res
}
})
}
至此,依赖收集 & 触发依赖 的功能就完成啦~