知识脉络

  1. JS是单线程的,同步任务在主线程上执行,而异步任务由eventloop机制管理,异步任务分两类,microtask和macrotask,具体机制是执行一个macrotask后,清空所有的microtask
  2. nextTick正是利用macrotask和microtask的机制,主线程进行所有组件的属性修改后,在microTask或microTask当中批量高效地进行更新。

Q:现在知道nextTick有一个队列保存着回调,问题是这些回调都是从哪里添加的?

A:Vue内部真正调用nextTick的只有一处:scheduler,这个scheduler保存着一个watcher的队列,通过queueWatcher函数来添加watcher,通过flushSchedulerQueue来清空watcher队列。

queueWatcher

  1. 如果存在重复watcher,则直接跳过
  2. 然后会判断是不是正在清空队列
  • 如果不是则直接添加watcher
  • 如果是,则通过 queue.splice(index+1, 0, watcher) 插入到队列当中,
    • 其中index是正在执行的watcher的下标或者未被执行的watcherid小于当前watcher的下标
  1. 最终判断是不是正在等待被执行,也就是判断flushSchedulerQueue有没有被nextTick添加进任务队列,如果没有则添加进去然后将waiting置为true

    1. if (!flushing) {
    2. queue.push(watcher)
    3. } else {
    4. // if already flushing, splice the watcher based on its id
    5. // if already past its id, it will be run next immediately.
    6. let i = queue.length - 1
    7. while (i > index && queue[i].id > watcher.id) {
    8. i--
    9. }
    10. queue.splice(i + 1, 0, watcher)
    11. }

    flushSchedulerQueue

    主要干了两件事

  2. 对queue进行升序排序,保证了一下几点

    1. 父节点比子节点先更新,因为父节点可能有属性需要传给子节点
    2. 用户的watcher比渲染watcher先执行,因为用户可能在watcher中再次修改组件属性
    3. 子组件被父组件卸载,其相关watcher可以不被执行
  3. 调用activated和updated钩子

    梳理

    以以下代码为例:
    1. new Vue({
    2. template: '<div @click="onClick">{{count}}</div>',
    3. data() {
    4. return {
    5. count: 1
    6. }
    7. },
    8. methods: {
    9. onClick() {
    10. this.count += 1
    11. Vue.nextTick(() => {
    12. console.log(this.count)
    13. })
    14. }
    15. }
    16. }).$mount('#app')
    当我们执行onClick时,由于this.count发生变化,所以会先将组件渲染的watcher通过queueWatcher函数添加到shceduler的queue当中,然后queueWatcher会执行 nextTick(flushSchedulerQueue) ,添加清理队列函数进nextTick队列中。Vue.nextTick也会将传入的函数添加到nextTick队列当中。当主线程清空,nextTick的队列开始清空,首先会执行的自然是flushSchedulerQueue,然后就是用户传入的回调。