详解vue nextTick原理

nextTick: 保证在dom更新 执行回调函数。

先有问题再有答案

  1. vue中的dom什么时候更新完
  2. 为什么不是保证dom渲染完成
  3. dom更新和浏览器渲染是一回事嘛?
  4. 更新这个是多久?
  5. 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渲染完成

要回答这个问题 首先需要一些前置知识:

  1. 浏览器:帧原理&渲染优化的基石
  2. js三座大山之异步五基于异步的js性能优化
  3. 浏览器的一帧&js执行&页面渲染。
    浏览器会清空执行栈中的同步JavaScript任务。一旦执行栈为空,它会查看任务队列,然后执行所有的微任务,如果在之前的JavaScript代码中查询了某些特定的需要最新布局信息的属性或方法(如:offsetHeight、getComputedStyle()等),浏览器会触发回流和重绘立即渲染,否则继续执行任务队列中的任务,直至一帧末尾开始渲染页面。

总结一下 浏览器的渲染是异步的。当我们通过js修改dom时 dom树在内存中是同步发生更新的,但是此时的最新状态并不会立即反应到屏幕上 而是要等待浏览器的渲染周期和帧率有关 一般在16.6ms 当渲染完成后 才能在屏幕观测到最新的页面。

所以在不使用任何框架的前提下 dom更新是同步的 渲染是异步的

在vue的框架下 通过数据更改dom这个过程也变成了异步。即

所以也就有了nextTick不能保证UI在屏幕中渲染完毕。只能保证在内存中有了.

当nextTick的回调函数被执行的时候,DOM已经在内存中完成了更新,状态已经被反映到DOM结构上,但在实际的显示器上可能尚未渲染出最新的状态。

当你需要在某个DOM更新后做一些事情,而这些事情依赖于渲染结果,最安全的方法仍然是使用requestAnimationFrame。此API提供了一种方式,可以让浏览器在下次重绘之前调用指定的回调函数。这通常是在屏幕刷新的每一帧中进行的。

更新这个是多久?

  1. 微任务:和dom更新在一个事件循环中执行nextTick的回调
  2. 宏任务:在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

相关推荐
viqecel7 分钟前
页面滚动下拉时,元素变为fixed浮动,上拉到顶部时恢复原状,js代码以视频示例
前端·javascript·ajax·滚动下拉浮动
caterpillarxie15 分钟前
第 3 章 HTML5 编程基础教案
前端·html·html5
Pandaconda24 分钟前
【新人系列】Python 入门(二十五):Socket 网络编程
开发语言·网络·笔记·后端·python·面试·网络编程
半兽先生39 分钟前
vue video重复视频 设置 srcObject 视频流不占用资源 减少资源浪费
前端·javascript·vue.js
A雄1 小时前
2025新春烟花代码(二)HTML5实现孔明灯和烟花效果
前端·javascript·html
uhakadotcom1 小时前
YC:2025年不容错过的1000个硬科技、新质生产力的创新方向清单
前端·面试·github
咔咔库奇1 小时前
ES6的高阶语法特性
前端·ecmascript·es6
一点一木1 小时前
Can I Use 实战指南:优化你的前端开发流程
前端·javascript·css
Fᴏʀ ʏ꯭ᴏ꯭ᴜ꯭.1 小时前
HTML前端从零开始
前端·html
博客zhu虎康1 小时前
Vue 封装公告滚动
前端·javascript·vue.js