Vue.$nextTick()干货详解

225 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

本文需要你了解 事件循环机制、微任务、宏任务。都是干货,希望你耐心看完。

案例代码

点击运行看结果

<template>
  <div>
    <div ref="count" >{{count}}</div>
    <div>
      <div>输出结果:</div>
      <ol>
        <li v-for="item in resList" :key="item">{{item}}</li>
      </ol>
    </div>
  </div>
</template>

<script>
// import Vue 不是必须的,需要手动指定 Vue 的版本时,可以解开注释
// import Vue from 'vue';

export default {
  data() {
    return {
      count: 0,
      resList:[]
    };
  },
  mounted(){
    this.resList.push(`sync console1 innerHTML: ${this.$refs.count.innerHTML}`);
    console.log(`sync console1 innerHTML: ${this.$refs.count.innerHTML}`) 
    this.count = 1;
    this.count = 2;
    this.resList.push(`sync console2 innerHTML: ${this.$refs.count.innerHTML}`);
    console.log(`sync console2 innerHTML: ${this.$refs.count.innerHTML}`) 
    this.$nextTick(()=>{
      this.resList.push(`nextTick1 innerHTML: ${this.$refs.count.innerHTML}`);
      console.log(`nextTick1 innerHTML: ${this.$refs.count.innerHTML}`) 
    })
    this.count = 3;
    Promise.resolve().then(()=>{
      this.resList.push(`Promise innerHTML: ${this.$refs.count.innerHTML}`);
      console.log(`Promise innerHTML: ${this.$refs.count.innerHTML}`) 
    })
    this.$nextTick(()=>{
      this.resList.push(`nextTick2 innerHTML: ${this.$refs.count.innerHTML}`);
      console.log(`nextTick2 innerHTML: ${this.$refs.count.innerHTML}`) 
    })
    // this.count = 4;
    this.resList.push(`sync console3 innerHTML: ${this.$refs.count.innerHTML}`);
    console.log(`sync console3 innerHTML: ${this.$refs.count.innerHTML}`) 
  }
};
</script>

按照上面的测试代码看到如下输出

sync console1 innerHTML: 0
sync console2 innerHTML: 0
sync console3 innerHTML: 0
nextTick1 innerHTML: 3
nextTick2 innerHTML: 3
Promise innerHTML: 3

这时会有几个问题:

  1. 为什么sync console2输出的不是count2
  2. 为什么nextTick1输出的不是count值2
  3. 为什么Promise输出在nextTick2后面?

从源码里找答案

我们都知道Vue2是用Object.defineProperty做响应式的,当我们执行 this.count = 1; 的时候,会触发set方法,所以我们就先从源码这里看。

set方法的最后执行了dep.notify(); 通知更新,但这时并不会马上更新页面,继续进入 notify() 内部看。

image.png

在这次测试代码中,this.subs是这个count对应的watcher,因为我们没有使用 $watch ,所以this.subs里只有一个watcher,并且它是负责更新这个组件的。

再进入到sub.update(); 内部看看。

image.png

如果是计算属性的watcher,会有lazy属性。$watch 会使用sync属性。我们只是单纯赋值,所以走queueWatcher(this) ,再进入内部看看。

image.png

has是一个对象,执行到这里会判断watcher是否重复,也就是说,我们测试代码中给count重复赋值,只有第一次count = 1; 时,组件的watcher会添加到 queue 中。

queue是负责存储 watcher 的一个数组。

flushing 是判断当前是否处于更新dom的过程中,如果是则插入到queue中,不是就直接push进去。

waiting 则表示在此次事件循环中,只执行一次。

flushSchedulerQueue 就是执行清空 queue 里的watcher,执行对应更新函数的方法,同时初始化 has = {};waiting = flushing = false;queue.length = 0;

nextTick 这个和我们使用的 Vue.$nextTick(),这里将 flushSchedulerQueue 作为回调函数传进去。

我们进入到 nextTick 内部看看。

image.png

callbacks 是一个数组。

timerFunc 是一个开启异步函数,它的实现是 Promise.resolve() 微任务 > MutationObserver 微任务 > setImmediate微任务 > setTimeout 宏任务

timerFunc方法会异步执行flushCallbacks方法,flushCallbacks方法会遍历执行 callbacks 里的回调函数,同时置空。

总结

  1. 第一个问题,因为每次赋值后更新页面是一个异步过程,所以 console2 输出的还是 0;
  2. 第二个问题,nextTick会将回调函数塞到callbacks中,而callbacks清空执行是异步的,所以实际上 this.count = 3;先执行。
  3. 第三个问题,因为当this.count = 1;时,此时的微任务队列中已经有了一个 timerFunc 方法负责,即使Promise 写在Vue.nextTick之前,但它的回调函数在timerFunc之后,而Vue.nextTick之前,但它的回调函数在 *timerFunc*之后,而Vue.nextTick会直接将回调函数塞到 timerFunc 方法中的 callbacks 数组中,所以Promise在后面执行。

思考

如果将

Promise.resolve().then(()=>{
      this.resList.push(`Promise innerHTML: ${this.$refs.count.innerHTML}`);
      console.log(`Promise innerHTML: ${this.$refs.count.innerHTML}`) 
    })

写到 this.count = 1; 之前,它和nextTick1哪个先执行?