Appearance
响应式数据伴侣 - reactiveEffect
构造函数
在之前的的实现中,effect函数是这样的
- effect
ts
export let activeSub = null
// effect 函数用于注册副作用函数
// 执行传入的函数,并在执行期间自动收集依赖
export function effect(fn) {
// 设置当前活跃的副作用函数,方便在 get 中收集依赖
activeSub = fn
// 执行副作用函数,此时会触发依赖收集
fn()
// 清空当前活跃的副作用函数
activeSub = null
}但是实际上,vue中需要考虑的问题比较多,所以 effect 函数中创建了一个类的实例,这个类就是 ReactiveEffect
- effect
ts
class ReactiveEffect {
constructor(public fn) {}
run() {
activeSub = this
try {
return this.fn()
} finally {
// fn 执行完毕后将 activeSub 回收
activeSub = undefined
}
}
}
export function effect(fn) {
// 创建一个 ReactiveEffect 实例
const e = new ReactiveEffect(fn)
e.run() // 执行 fn
}那么此时对应的propagate函数中的依赖触发也要修改,因为此时activeSub已经变成了一个对象
- system.ts
ts
/*
* 传播更新的函数
* @param subs
*/
export function propagate(subs) {
let link = subs
let queuedEffect = []
while (link) {
queuedEffect.push(link.sub)
link = link.nextSub
}
queuedEffect.forEach((effect) => effect.run())
}嵌套effect
看一个案例:
ts
const count = ref(0)
// effect1
effect(() => {
//effect2 在 effect1 中执行
effect(() => {
console.log('effect2', count.value)
})
console.log('effect1', count.value)
})
setTimeout(() => {
count.value = 1
}, 1000)这个时候输出结果是:effect2,1
我们期望的是输出:effect2,1和effect1, 1
看一下原因:
ts
class ReactiveEffect {
// 表示当前是否被激活,如果为false则不收集依赖
active = true
constructor(public fn) {}
run() {
// 如果当前的effect未激活,那么就不收集依赖,直接返回fn的结果
if(!this.active) {
return this.fn()
}
// 得将当前的fn保存到全局,以便于收集依赖
activeSub = this
try {
return this.fn()
} finally {
// fn 执行完毕后将 activeSub 回收
activeSub = undefined // 🚨 fn 执行完毕后,被置空了
}
}
}activeSub = undefined看一下这行代码,乍一看,似乎没有什么问题。- 但是,回到案例中看一下,当
effect1执行的时候,activeSub = effect1,然后effect1中又创建了一个effect2,此时,执行effect2的run方法,然后马上activeSub又变成了effect2,等effect2执行完毕后,将activeSub设置为undefined,但是此时effect1还没执行完。后面访问ref就不会被收集了。 - 所以当一个
effect执行完毕后,不能把它设置为undefined。 - 在
activeSub = this,也就是effect2执行之前,activeSub是有值的,在effect2执行前,它的值是effect1,把它存起来,等activeSub执行完毕后,在重新赋值。
ts
class ReactiveEffect {
// 表示当前是否被激活,如果为 false 则不收集依赖
active = true
constructor(public fn) {}
run() {
// 💡 保存之前的 activeSub
const prevSub = activeSub
// 将当前的 effect 保存到全局,以便于收集依赖
activeSub = this
try {
return this.fn()
} finally {
// 💡 fn 执行完毕后将 activeSub 恢复为 prevSub
activeSub = prevSub
}
}
}- 这样在执行
activeSub = this之前,先将他保存起来,等fn执行完毕后,在将它重新恢复,这样嵌套的问题就解决了。 - 如果没有嵌套的情况下,第一次执行的时候,
prevSub就是undefined。
ts
const count = ref(0)
// effect1
effect(() => {
// 🚨 effect2 在 effect1 中执行
effect(() => {
console.log('effect2', count.value)
})
console.log('effect1', count.value)
})
setTimeout(() => {
count.value = 1
}, 1000)此时这段代码在定时器修改完后会正常打印出:
- 'effect2' 1
- 'effect1' 1
调度器(scheduler)
调度器是响应式系统一个重要概念,我们默认使用effect访问响应式属性的时候,会收集依赖,修改响应式属性后,这个effect的fn会重新执行,而scheduler的作用是,当响应式数据发生变化的时候,执行scheduler,而不是重新执行fn。在创建effect的时候,还是会执行fn,因为要靠他收集依赖。
ts
const count = ref(0)
effect(() => {
console.log('在 fn 中收集了依赖', count.value)
}, {
scheduler() {
console.log('scheduler', count.value)
}
})
setTimeout(() => {
// ⭐️ 由于传递了 scheduler ,所以我们更新响应式属性的时候,会触发 scheduler
count.value++ // scheduler
}, 1000)如何实现功能:
- 默认:
effect在创建时会执行一次fn,当fn中访问的响应式数据发生变化时,会重新执行,无论初始化,还是数据发生变化,都会重新执行fn - 调度器:当传递了
scheduler,首次创建effect的时候,依然会执行fn,但是当数据发生变化的时候,就会执行scheduler,也就是说响应式数据更新的时候,不能执行fn了。 - 或者说可以这样,
ReactiveEffect本身就存在scheduler,这个方法默认会调用run方法,但是如果传递了scheduler,调用对象本身的scheduler
ts
class ReactiveEffect {
constructor(public fn) {}
run() {
const prevSub = activeSub
// 每次执行 fn 之前,把 this 放到 activeSub 上面
activeSub = this
try {
return this.fn()
} finally {
// 执行完成后,恢复之前的 effect
activeSub = prevSub
}
}
/**
* 默认调用 run,如果用户传了,那以用户的为主,实例属性的优先级,由于原型属性
*/
scheduler() {
this.run()
}
}
export function effect(fn, options) {
const e = new ReactiveEffect(fn)
// 将传递的属性合并到 ReactiveEffect 的实例中
Object.assign(e, options)
// 执行 run 方法
e.run()
}此时需要修改propagate中执行的方法也需要修改一洗,因为之前执行的是run方法
搞一个 notify 方法,我只管调用你的 notify 方法,至于你最终执行那个方法,你自己决定:
ts
/**
* 传播更新的函数
* @param subs
*/
export function propagate(subs) {
// 省略部分代码...
// 这里执行 notify 方法
queuedEffect.forEach((effect) => effect.notify())
}在ReactiveEffect 中添加一个notify方法
ts
class ReactiveEffect {
// 省略部分代码...
/**
* 通知更新的方法,如果依赖的数据发生了变化,会调用这个函数
*/
notify() {
this.scheduler()
}
// 省略部分代码...
}总结:
因为例属性优先级,优于原型属性 所以在用户传了scheduler的时候,notify中this.scheduler,就调用了传入scheduler。在没有传入的时候,调用了内部scheduler,然后内部scheduler调用run