nextTick,事件循环

nextTick,事件循环


vue nextTick 事件循环 前端

vue 中的 nextTick

nextTick 的作用是把传入的回调函数延迟到下次 DOM 更新循环之后执行。

原因(vue 异步更新策略)

因为 vue 采用的异步更新策略,当监听到数据发生变化的时候不会立即去更新 DOM, 而是开启一个任务队列,并缓存在同一事件循环中发生的所有数据变更; 这种做法带来的好处就是可以将多次数据更新合并成一次,减少操作 DOM 的次数;

因为如果同步进行 DOM 更新,则每次对响应式数据进行修改就都会触发setter -> 通知watcher -> 触发re-render -> 生成new vnode(vdom) -> patch(更新真实DOM)。如果每次修改数据都会走一遍这个流程是非常消耗性能的,所以使用异步更新 DOM 的策略,先对数据修改进行整合,再使用最终的整合结果一次性对 DOM 进行更新。

为了能够获取更新后的 dom,所以提供了 nextTick 这个 api。 注意:dom 更新不代表 dom 渲染,因为事件循环的机制是 宏任务 => 微任务 => 渲染 => 宏任务…,所以微任务 api 实现的 nextTick 时,可以获取到更改后的 dom,但此时 dom 还未渲染,可以使用 alert 验证。

vue2

处于兼容性考虑,依次判断浏览器是否支持,选择对应 api Promise (微任务) => MutationObserver (微任务) => setImmediate (大部分浏览器不支持) => setTimeout (宏任务)

setTimeout 会产生一个宏任务,在该任务中执行回调,相比较微任务,页面会多渲染一次。

注意: 如果 setTimeout()的延迟时间设置为 0,它实际上仍需要一定的时间来执行 setTimeout(fn, 0)会被强制为 setTimeout(fn, 1), 将在 1 毫秒后执行。

相比之下,setlmmediate 会将回调函数推入到事件循环的队列中,但是会在当前事件循环的末尾立即执行它。这意味着 setlmmediate 的回调函数总是会在 setTimeout 的回调函数之前执行,这种行为有助于避免可能的性能问题。

vue3

抛弃了兼容性,直接使用 Promise,来实现 nextTick

首先看一段代码:

<template>
  <div>
    <p class="name">{{ name }}</p>
    <button @click="modify">修改</button>
  </div>
</template>

<script type="text/javascript">
  export default {
    data() {
      return {
        name: "111",
      };
    },
    methods: {
      modify() {
        this.name = "222";
        this.$nextTick(() => {
          const text = document.querySelector(".name").innerText;
          console.log(text);
        });
        this.name = "333";
      },
    },
  };
</script>

此时点击”修改”按钮打印的结果是 “333”

修改一下上述代码

<template>
  <div>
    <p class="name">{{ name }}</p>
    <button @click="modify">修改</button>
  </div>
</template>

<script type="text/javascript">
  export default {
    data() {
      return {
        name: "111",
      };
    },
    methods: {
      modify() {
        // this.name = "222";
        this.$nextTick(() => {
          const text = document.querySelector(".name").innerText;
          console.log(text);
        });
        this.name = "333";
      },
    },
  };
</script>

仅仅注释掉 nextTick 前的赋值语句,此时打印的结果就是”111”。

结论

nextTick 本质就是创建了一个微任务(不考虑 setTimeout 的情况),将其回调推入微任务队列。vue 中一个事件循环中的所有 dom 更新操作也是一个微任务,两者属于同一优先级,执行先后只于入队的先后有关,换句话说,如果你先写了 nextTick,再写赋值语句(在此之前没有触发 dom 更新的操作),那在 nextTick 中获取的就不是更新后的 dom 了。

事件循环

这里仅仅是对一个问题做一下记录

<div class="outer">
 <div class="inner"></div>
</div>

<script>
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

function onClick() {
 console.log('click');

 setTimeout(function() {
   console.log('timeout');
 }, 0);

 Promise.resolve().then(function() {
   console.log('promise');
 });

 outer.setAttribute('data-random', Math.random());
}

inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
<script>

当点击 class 为 inner 的 div 块时,控制台依次输出结果:

click
promise
click
promise
timeout
timeout

我理解下来就是在点击的时候,会把所有 click 相关的回调依次放进宏任务队列,然后依次执行,执行到第一个 click 中的 setTimeout 时,会把 setTimeout 的回调再添加到往宏任务队列最后。

© 2025 Niko Xie