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 的回调再添加到往宏任务队列最后。