nextTick作用及原理

176 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第13天,点击查看活动详情

nextTick是做什么的?

nextTick 可以让我们在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM

为什么需要nextTick?其使用场景是什么?

了解nextTick之前,我们需要先了解一下Vue的异步更新策略。

Vue的异步更新策略:

当数据变化时Vue不会立刻更新DOM,而是开启⼀个队列,把组件更新函 数保存在队列中,在同⼀事件循环中发生的所有数据变更会异步的批量更新。这⼀策略导致我们对数据的修改不会立刻体现在DOM上,此时如果想要获取更新后的DOM状态,就需要使用nextTick。

使用场景:

例如在事件处理逻辑时希望获取更新后的dom状态,示例:

<template>
  <div>
    <h1 ref="msgRef">{{msg}}</h1>
    <button @click="setMsg">修改</button>
  </div>
</template>
<script>
export default {
  data() {
    return {
      msg: '123'
    };
  },
  methods: {
    setMsg() {
      this.msg = '设置的内容'
      console.log(this.$refs['msgRef'].innerHTML)  // 第一个输出
      this.$nextTick(function() {
        console.log(this.$refs['msgRef'].innerHTML)  // 第二个输出
      })
    }
  },
};
</script>

点击按钮后,第一个输出123,第二个输出设置的内容

nextTick签名如下: function nextTick(callback?: () => void): Promise 所以我们只需要在传入的回调函数中访问最新DOM状态即可,或者我们可以await nextTick⽅法返回的 Promise之后做这件事。

在Vue内部,nextTick之所以能够让我们看到DOM更新后的结果,是因为我们传⼊的callback会被添加到队列 刷新函数(flushSchedulerQueue)的后⾯,这样等队列内部的更新函数都执⾏之后,所有DOM操作也就结束 了,callback⾃然能够获取到最新的DOM值。

原理

nextTick 是 Vue 的一个核心实现,在介绍 Vue 的 nextTick 之前,为了方便大家理解,我先简单介绍一下 JS 的单线程事件循环机制

js主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 结束后,都要清空所有的 micro task。然后去循环这个过程(宏任务->清空微任务->宏任务->清空微任务......)

nextTick主要使用了宏任务和微任务的原理。根据执行环境分别尝试采用

  • Promise
  • MutationObserver
  • setImmediate
  • 如果以上都不行则采用setTimeout

在源码 src/core/util/next-tick.js 中:

  1. next-tick.js 申明了 microTimerFuncmacroTimerFunc 2 个变量,它们分别对应的是 micro task 的函数和 macro task 的函数。对于 macro task 的实现,优先检测是否支持原生 setImmediate,这是一个高版本 IE 和 Edge 才支持的特性,不支持的话再去检测是否支持原生的 MessageChannel,如果也不支持的话就会降级为 setTimeout 0;而对于 micro task 的实现,则检测浏览器是否原生支持 Promise,不支持的话直接指向 macro task 的实现。
  2. next-tick.js 对外暴露了 2 个函数,先来看 nextTick,这就是我们在上一节执行 nextTick(flushSchedulerQueue) 所用到的函数。它的逻辑也很简单,把传入的回调函数 cb 压入 callbacks 数组,最后一次性地根据 useMacroTask 条件执行 macroTimerFunc 或者是 microTimerFunc,而它们都会在下一个 tick 执行 flushCallbacksflushCallbacks 的逻辑非常简单,对 callbacks 遍历,然后执行相应的回调函数。
  3. 这里使用 callbacks 而不是直接在 nextTick 中执行回调函数的原因是保证在同一个 tick 内多次执行 nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。

源码直达:

更新函数入队

入队函数

nextTick定义

在源码中定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列

  • Vue 2.4 之前都是使用的 microtasks微任务,但是microtasks 的优先级过高,在某些情况下可能会出现比事件冒泡更快的情况,但如果都使用 macrotasks 又可能会出现渲染的性能问题。所以在新版本中,会默认使用 microtasks,但在特殊情况下会使用 macrotasks,比如 v-on
  • 对于实现 macrotasks ,会先判断是否能使用 setImmediate ,不能的话降级为 MessageChannel ,以上都不行的话就使用 setTimeout

如何判断能不能使用相应的API

if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else if (
  typeof MessageChannel !== 'undefined' &&
  (isNative(MessageChannel) ||
    // PhantomJS
    MessageChannel.toString() === '[object MessageChannelConstructor]')
) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

总结:

  • 新版本中默认是mincrotasks, v-on中会使用macrotasks

  • macrotasks宏任务的实现:

    • setImmediate / MessageChannel / setTimeout