Vue3响应式系统(二)https://blog.csdn.net/qq_55806761/article/details/135612738
七、无限递归循环。
响应式系统里无限递归也是需要考虑到的。
什么情况会出现无限递归循环?
代码示例:
const data = { foo: 1 }
const obj = new Proxy(/* * */)
effect(() => {
obj.foo++
})
obj.foo++会直接导致栈溢出,如图所示:
为何会这样呢?
obj.foo++ ====> obj.foo = obj.foo + 1 ,首先会读取obj.foo的值,这就会触发get函数中的track收集函数,之后又会将obj.foo的值赋值给obj.foo,这时又会触发trigger触发函数。读取触发的副作用函数还没有执行完,又要去设置执行副作用函数,无限循环的去调用自己,所以产生了栈溢出。 所以,既会读取值,又会设置值,这就是出现栈溢出的根本原因。
如何解决呢?
由于读取和设置都是在同一个副作用函数中执行的,无论是track收集到的还是trigger触发的都是activeEffect,不变。所以,我们可以在trigger增加一个守卫:如果trigger触发的执行的副作用函数与当前正在执行的副作用函数一样,便不触发执行。
更改trigger触发函数:
function trigger(target, key) {
const depsMap = bucketMap.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectToRun = new Set()
//增加守卫
effects && effects.forEach(effectFn => {
if(effectFn != activeEffect) {
effectToRun.add(effectFn)
}
})
effectToRun && effectToRun.forEach(effectFn => effectFn())
}
此时,便不会无限循环,而是只执行一次。
八、调度执行。
可调度性是响应式系统非常重要的的特性。
可调度性?
简单来说就是,trigger触发副作用函数执行的时候,可以去决定副作用函数执行的时机、次数以及方式。
示例:(结果如图所示)
// 依据上文扩展案例
effect(() => {
console.log(obj.foo);
})
obj.foo++
console.log('结束了')
如果我要实现下面的效果呢?(不改变代码顺序)
这时就需要响应式系统支持可调度。我们可以为effect函数设置一个选项参数options,允许用户执行调度器。
effect(
() => {
console.log(obj.foo);
},
// options
{
// 调度器 scheduler 是一个函数
scheduler(fn) {
// ...
}
}
)
更改effect函数
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
// 将 options 挂载到 effectFn 上
effectFn.options = options
effectFn.deps = []
effectFn()
}
根据options,就要来更改trigger函数了
function trigger(target, key) {
const depsMap = bucketMap.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectToRun = new Set()
effects && effects.forEach(effectFn => {
if(effectFn != activeEffect) {
effectToRun.add(effectFn)
}
})
effectToRun && effectToRun.forEach(effectFn => {
// 如果存在调度器,则调用这个调度器,并将副作用函数作为参数传递
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else { // 没有调度器则直接执行副作用函数
effectFn()
}
})
}
去测试:将副作用函数加入宏任务队列中执行
effect(
() => {
console.log(obj.foo);
},
// options
{
// 调度器 scheduler 是一个函数
scheduler(fn) {
// 将副作用函数加入宏任务队列
setTimeout(fn)
}
}
)
console.log('结束了')
obj.foo++
如图所示,效果得以实现。
除了调度执行顺序,还可以做到调度执行次数。
effect(
() => {
console.log(obj.foo);
}
)
obj.foo++
obj.foo++
/**
* 打印结果为:
* 1
* 2
* 3
*/
从1到3,2只是过渡,我们并不关心,所以执行三次有点多余。我们希望只打印:(不包含过度)
/**
* 打印结果为:
* 1
* 3
*/
基于调度器我们实现一下子:
//定义一个任务队列
const jobQueue = new Set()
// 使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列中
const p = Promise.resolve()
//一个标志代表是否正在刷新队列
let isFlushing = false
function flushJob() {
// 如果队列正在刷新, 则什么都不做
if(isFlushing) return
// 正在刷新设置
isFlushing = true
// 在微任务对类中刷新JobQueue队列
p.then(() => {
jobQueue.forEach(job => job())
}).finally(() => {
//结束后重置 isFlushing
isFlushing = false
})
}
// 测试
effect(
() => {
console.log(obj.foo);
},
{
scheduler(fn) {
jobQueue.add(fn)
flushJob()
}
}
)
obj.foo++
obj.foo++
/**
* 打印:
* 1
* 3
*/
当obj.foo执行两次自增操作,会同步且连续执行两次scheduler调度函数,这意味着同一个副作用函数会被jobQueue.add(fn)语句添加两次,由于Set去重能力,最终jobQueue中只会有一项,即当前副作用函数。同理,flushJob也会同步执行两次,由于isFlushing标志的存在,实际上flushJob函数在一个事件循环内只会执行一次,即在微任务队列内执行一次。当微任务队列开始执行的时候,jobQueue队列就会遍历里面存的副作用函数,当副作用函数调用的时候obj.foo已经是3了,这样我们就实现了所期望的值。
其实这个功能类似于Vue.js中连续多次修改响应式数据但只会触发一次更新,实际上Vue.js内部实现了一个更加完善的调度器,思路与上面代码思路相同。