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

相关推荐
林的快手1 小时前
CSS列表属性
前端·javascript·css·ajax·firefox·html5·safari
匹马夕阳2 小时前
ECharts极简入门
前端·信息可视化·echarts
Nicole Potter2 小时前
请说明C#中的List是如何扩容的?
开发语言·面试·c#
API_technology2 小时前
电商API安全防护:JWT令牌与XSS防御实战
前端·安全·xss
yqcoder2 小时前
Express + MongoDB 实现在筛选时间段中用户名的模糊查询
java·前端·javascript
十八朵郁金香2 小时前
通俗易懂的DOM1级标准介绍
开发语言·前端·javascript
计算机-秋大田3 小时前
基于Spring Boot的兴顺物流管理系统设计与实现(LW+源码+讲解)
java·vue.js·spring boot·后端·spring·课程设计
m0_528723813 小时前
HTML中,title和h1标签的区别是什么?
前端·html
Dark_programmer3 小时前
html - - - - - modal弹窗出现时,页面怎么能限制滚动
前端·html
GDAL4 小时前
HTML Canvas clip 深入全面讲解
前端·javascript·canvas