详解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

相关推荐
也无晴也无风雨27 分钟前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang1 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
SRY122404192 小时前
javaSE面试题
java·开发语言·面试
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、4 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
不二人生5 小时前
SQL面试题——连续出现次数
hive·sql·面试
郝晨妤5 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui