nextTick: 保证在dom更新
后
执行回调函数。
先有问题再有答案
vue中的dom什么时候更新完
为什么不是保证dom渲染完成
dom更新和浏览器渲染是一回事嘛?
更新
后这个
后是多久?
vue 源码是如何实现的
如果以上问题都可以回答上 那么没必要再读下去了...
vue中的dom什么时候更新完
看下官网的例子:
xml
<script setup>
import { ref, nextTick } from 'vue'
const count = ref(0)
async function increment() {
count.value++
// DOM 还未更新
console.log(document.getElementById('counter').textContent) // 0
await nextTick()
// DOM 此时已经更新
console.log(document.getElementById('counter').textContent) // 1
}
</script>
<template>
<button id="counter" @click="increment">{{ count }}</button>
</template>
当你在 Vue 中更改响应式状态时,最终的 DOM 更新并不是同步生效
的,而是由 Vue 将它们缓存在一个队列
中,这样是为了确保每个组件无论发生多少状态改变,都仅执行一次更新。
所以dom是否更新完这个是由vue保证的,内部的队列执行完成,dom也就更新完了。
为什么不是保证dom渲染完成
要回答这个问题 首先需要一些前置知识:
- 浏览器:帧原理&渲染优化的基石
- js三座大山之异步五基于异步的js性能优化
- 浏览器的一帧&js执行&页面渲染。
浏览器会清空执行栈中的同步JavaScript任务。一旦执行栈为空,它会查看任务队列,然后执行所有的微任务,如果在之前的JavaScript代码中查询了某些特定的需要最新布局信息的属性或方法(如:offsetHeight、getComputedStyle()等),浏览器会触发回流和重绘立即渲染,否则继续执行任务队列中的任务,直至一帧末尾开始渲染页面。
总结一下 浏览器的渲染是异步的
。当我们通过js修改dom时 dom树在内存中是同步发生更新的,但是此时的最新状态并不会立即反应到屏幕上 而是要等待浏览器的渲染周期和帧率有关 一般在16.6ms 当渲染完成后 才能在屏幕观测到最新的页面。
所以在不使用任何框架的前提下 dom更新是同步的 渲染是异步的
在vue的框架下 通过数据更改dom这个过程也变成了异步。即
所以也就有了nextTick不能保证UI在屏幕中渲染完毕。只能保证在内存中有了
.
当nextTick的回调函数被执行的时候,DOM已经在内存中完成了更新,状态已经被反映到DOM结构上,但在实际的显示器上可能尚未渲染出最新的状态。
当你需要在某个DOM更新后做一些事情,而这些事情依赖于渲染结果,最安全的方法仍然是使用requestAnimationFrame。此API提供了一种方式,可以让浏览器在下次重绘之前调用指定的回调函数。这通常是在屏幕刷新的每一帧中进行的。
更新后
这个后
是多久?
- 微任务:和dom更新在一个事件循环中执行nextTick的回调
- 宏任务:在dom更新后的下一个事件循环执行nextTick的回调
nextTick到底使用宏任务还是微任务 这是一个有点复杂的问题
具体可以看这篇文章 Vue的nextTick具体是微任务还是宏任务? 的总结。
最终优先使用了微任务...
源码&注释
javascript
/* @flow */
/* globals MutationObserver */
import { noop } from 'shared/util';
import { isIE, isIOS, isNative } from './env';
import { handleError } from './error';
// 是否正在使用微任务
export let isUsingMicroTask = false;
// 回调函数队列
const callbacks = [];
// 是否有一个待处理的微任务标志
let pending = false;
// 处理回调队列的函数
function flushCallbacks() {
pending = false;
// 将callbacks中的函数拷贝一份来执行
const copies = callbacks.slice(0);
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}
// 我们在这里有使用微任务延迟的包装器。
// 在2.5版本中我们使用了宏任务(结合微任务)。
// 然而,当状态变化紧接在重绘之前发生时,这会出现一些细微的问题
// (例如 #6813, out-in transitions)。
// 同样,在事件处理函数中使用宏任务也会导致一些无法规避的奇怪行为
// (例如 #7109, #7153, #7546, #7834, #8109)。
// 因此我们现在又重新在所有地方使用微任务。
// 采取这种折中方案的主要缺点是有些场景下微任务优先级过高,
// 导致它们在本应连续的事件之间(例如 #4521, #6690,它们有对应的解决方法)
// 甚至在同一个事件的冒泡之间获取执行权(#6566)。
let timerFunc;
// nextTick的行为利用了微任务队列,可以通过原生的Promise.then或MutationObserver访问。
// MutationObserver支持的范围更广,然而它在iOS >= 9.3.3的UIWebView中触发触摸事件处理器时严重有缺陷。
// 它在触发几次后就完全停止工作了...所以,如果原生的Promise可用,我们将使用它:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
// 在有问题的UIWebViews中,Promise.then不会完全失效,但
// 可以会卡在一个奇怪的状态,微任务队列被推送了回调但不会被清空,
// 直到浏览器需要处理一些其他工作,例如处理一个计时器。因此,我们可以
// "强行"通过添加一个空计时器来清空微任务队列。
if (isIOS) setTimeout(noop);
};
isUsingMicroTask = true;
} else if (
!isIE &&
typeof MutationObserver !== 'undefined' &&
(isNative(MutationObserver) ||
// PhantomJS和iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
// 在原生Promise不可用时使用MutationObserver,
// 例如 PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver在IE11中不稳定)
let counter = 1;
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true,
});
timerFunc = () => {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 回退到setImmediate。
// 从技术上讲,它利用的是宏任务队列,
// 但它仍然是比setTimeout更好的选择。
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 回退到setTimeout。
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
// nextTick函数,用于把一个回调推到下一个tick执行
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve;
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
timerFunc();
}
// 如果没有提供回调,并且Promise可用,则返回一个Promise
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve;
});
}
}
其他相关文章:
为什么react需要fiber&时间分片而vue没有?听听尤大怎么说
多图讲解vue3快速diff算法
多图讲解Vue3的diff算法最长递增子序列实现原理
key可以重复嘛?为什么不使用index做key