likes
comments
collection
share

详解vue nextTick原理

作者站长头像
站长
· 阅读数 5

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

先有问题再有答案

  1. vue中的dom什么时候更新完
  2. 为什么不是保证dom渲染完成
  3. dom更新和浏览器渲染是一回事嘛?
  4. 更新这个是多久?
  5. vue 源码是如何实现的

如果以上问题都可以回答上 那么没必要再读下去了...

vue中的dom什么时候更新完

看下官网的例子:

<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修改dom时 dom树在内存中是同步发生更新的,但是此时的最新状态并不会立即反应到屏幕上 而是要等待浏览器的渲染周期和帧率有关 一般在16.6ms 当渲染完成后 才能在屏幕观测到最新的页面。

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

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

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

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

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

更新这个是多久?

  1. 微任务:和dom更新在一个事件循环中执行nextTick的回调
  2. 宏任务:在dom更新后的下一个事件循环执行nextTick的回调

源码&注释

/* @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;
        });
    }
}

其他相关文章: