持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情
设计一个完善的响应系统
上一节中,我们已经实现了一个微型响应系统。这次我们将尝试构造一个更加完善的响应系统。
实现一个响应系统需要以下的工作流程:
- 当读取操作发生时,将副作用函数收集到桶中;
- 当设置操作发生时,从桶中取出副作用函数并执行
上一节中我们硬编码了一个effect函数,并将其收集到桶中,而这并不符合响应系统的要求,我们要实现的是任何函数,包括匿名函数都能被收集到。
// 用一个全局变量存储被注册的副作用函数
let activeEffect
// effect 函数用于注册副作用函数
function effect(fn) {
// 当调用effect 注册副作用函数时,将副作用函数fn赋值给activeEffect
activeEffect = fn
// 执行副作用函数
fn()
}
首先我们定义一个全局变量activeEffect,初始值是undefined,它的作用是存储被注册的副作用函数。接下来我们就需要改变effect函数,使之成为一个用来注册副作用函数的函数,effect函数接受一个参数fn,即要注册的副作用函数。
effect(
// 一个匿名的副作用函数
() => {
document.body.innerText = obj.text
}
)
这里,我们使用了一个匿名函数作为effect函数的参数。当effect执行的时候,首先把匿名副作用函数fn赋值给全局变量activeEffect。接着执行被注册的匿名副作用函数,这时就会触发响应式数据的obj.text的读取操作,进而触发代理对象Proxy的get拦截:
const obj = new Proxy(data, {
get(target, key) {
// 将activeEffect中存储的副作用函数收集到桶中
if (activeEffect) { // 新增
bucket.add(activeEffect) // 新增
} // 新增
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
bucket.forEach(fn => fn())
return true
}
})
上述代码中,副作用已经存储到了activeEffect中,所以在get拦截的时候,函数内应该把activeEffect收集到桶中,这样响应式便不再依赖副作用函数的名字了。
但是继续进行时,我们在响应式数据obj中添加一个不存在的属性时:
effect(
// 匿名副作用函数
() => {
console.log('effect run'); // 会打印两次
document.body.innerText = obj.text
}
)
setTimeout(() => {
// 副作用函数中并没有读取 notExist 属性的值
obj.notExist = 'hello vue3'
}, 1000);
匿名副作用函数内部读取了字段obj.text值,此时匿名副作用函数与字段obj.text之间建立响应联系。接着,我们开启一个定时器,在一秒钟之后为对象obj,添加新的属性。理论上,在副作用函数内部没有读取到obj.notExist属性的值,因此字段obj.notExist不应该与匿名副作用函数建立响应式联系,定时器内部的语句不应该触发副作用函数的执行。但是实际上我们操作会发现,匿名副作用函数确实是执行了,这显然不是我们响想要的。因此我们必须重新设计一个桶的数据结构去实现需求的功能。
那么如何去实现这一功能呢,我们将在下一篇文章中为大家解开谜题。