代码例子
const { createApp } = Vue const app = createApp({ components: [], template: ` <div> <button @click="handleClick">toggle</button> </div> `, data() { return { visible: false, test: 1, } }, methods: { handleClick() { this.visible = true; // 代码1 this.visible = false; // 代码2 this.visible = true; // 代码3 }, }, watch: { visible(v) { console.log('visible change', v) }, } }).mount('#app')
上述代码点击toggle后的效果如下图。visible change只log了一次。
通过ReactiveEffect监听改变
Vue 通过实例化 ReactiveEffect ,监听getter的改变,回调scheduler。通过这种方式去劫持 visible 的更改。
// packagesruntime-coresrcapiWatch.ts let scheduler: EffectScheduler if (flush === 'sync') { scheduler = job as any } else if (flush === 'post') { scheduler = () => queuePostRenderEffect(job, instance && instance.suspense) } else { job.pre = true if (instance) job.id = instance.uid scheduler = () => queueJob(job) } const effect = new ReactiveEffect(getter, scheduler) // getter在本场景中对应visible。scheduler会在getter更改时被调用
代码1执行 第一次赋值
代码 1 执行后, visible 设置为 true 。Vue 监听到 visible 的更改。执行 queueJob ,将负责执行回调函数的 job 推入 queue 。并利用 Promise.then 的回调函数会被放入微任务队列的特性,使用 then 将执行 queue 中所有方法的任务放入微任务队列中。
// packagesruntime-coresrcscheduler.ts export function queueJob(job: SchedulerJob) { console.log('queueFlush called') // 检查queue是否为空、job是否存在queue中 if ( !queue.length || !queue.includes( job, isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex ) ) { if (job.id == null) { queue.push(job) } else { queue.splice(findInsertionIndex(job.id), 0, job) } queueFlush() } } function queueFlush() { if (!isFlushing && !isFlushPending) { isFlushPending = true currentFlushPromise = resolvedPromise.then(flushJobs) // flushJobs进入微任务 } }
代码2执行 第二次赋值
代码 2 执行后, visible 设置为 false。Vue 知道 visible 更改后,发现 负责 visible 更新时回调的 job 已经推入 queue,于是不再继续执行推入操作。
代码3执行 第三次赋值
代码 3 执行后,visible 设置为 true。后面同上,不再继续执行推入操作。
handleClick 执行完毕
handleClick 执行完毕。事件循环机制会去执行微任务队列的任务。然后代码1执行后放入微任务队列中的 job 得到了执行,回调函数也得到了执行。
综上所述
综上所述,job因为只推入1次,所以只执行了一次,监听回调也只执行了一次。