Vue3 nextTick 真的能拿到最新的DOM么?

1,504 阅读2分钟

Vue3 nextTick 本质是优先在本轮事件循环的微任务阶段异步执行逻辑。如果浏览器不支持微任务,则在下一轮事件循环后异步执行。

一. 特性一览

image.png

二. 伪代码实现

if(当前环境支持 Promise){
    Promise.then 派发 timerFunc
} else if (当前环境支持 MutationObserver) {
    MutationObserver 派发 timerFunc
} else if (当前环境支持 setImmediate) {
    setImmediate 派发 timerFunc
} else {
    setTimeout 派发 timer Func
}

三. 为什么要优先选择微任务?

  1. 在程序流中更早的得到执行:微任务会在本轮事件循环结束前执行,优于下一轮宏任务的执行。
  2. 避免竞态条件:宏任务种类很多,随时可能会有定时器,网络请求等任务穿插,nextTick作为微任务可以确定出在宏任务之前执行。
  3. 与Vue2的设计保持一致。

四. nextTick 真的能拿到最新的DOM么?

网上几乎所有资料都说nextTick能拿到最新DOM的绝对性,

就连Vue官网也将 nextTick 定义为 “等待下一次 DOM 更新刷新的工具方法。”

如此权威的发言,让笔者的反对言论不胜惶恐。

基于笔者对Vue3源码的理解和思考,Vue3 的模板更新逻辑(即,组件的 render Effect),实际上也会作为微任务,被推进异步执行队列。而 nextTick 的回调也作为被微任务推入异步执行队列,和 render Effect 为伍,Vue3并没有对 nextTick 的回调和 render Effect 进行优先级排列,这就意味着:

  • nextTick的回调 若比 render Effect 更早的被推入微任务队列,那么 nextTick 的回调将优先于 render Effect 执行,此时将无法在 nextTick 回调中拿到最新的DOM!

为了验证笔者这一想法是否成真,笔者特别的将 官网提供的 nextTick 实例改造,并运行,结果证明我的结论并无错误:

template:

<template>
    <button id="counter" @click="increment">{{ count }}</button>
</template>

script:

<script setup>

import { ref, nextTick } from 'vue'

const count = ref(0)

async function increment() {
  // 将官网提供的nextTick提到 count.value++ 前执行
  nextTick(() => {
    /**
     * 将 nextTick 回调推入微任务队列的首位
     */
    console.log('second', document.getElementById('counter').textContent) // 0
  })
  /**
   * 触发响应式变量更新,该组件的 render effect 将从 count.value 的 depSet 中取出,
   * 并推入微任务队列中,此时 nextTick 回调在 render effect 前,render effect 在后。
   */
  count.value++

  /**
   * 下面这行代码是同步获取DOM,微任务队列还未启动执行,render effect未执行,无法得到最新DOM
   */
  console.log('first', document.getElementById('counter').textContent) // 0

  /**
   * 当前主执行栈中任务已空,开始执行微任务队列中的任务。
   * nextTick回调执行 -> render effect执行。
   * 
   * 当执行nextTick回调时,render effect同样未执行,DOM未来得及更新,因此nextTick回调
   * 中无法获取模板更新后的DOM,console值依旧是0!
   */
}

</script>

image.png