【面试题】NextTick 在 Vue 中如何发挥作用的?原理原来这么简单!

276 阅读4分钟

扯皮

在最近的面试中,不少面试官问道简单聊一下 vue 中的nextTick吧,nextTick是个啥,这篇文章咱来好好聊聊!

用通俗易懂的方式理解的话,可以说是由于 Vue 的响应式变量是异步更新 DOM 的,当你的数据变量发生改动时,不能第一时间更新到最新的 DOM 中,这时候呢,这个nextTick就可以拿到最新的 DOM。

示例 demo

在这个例子中,我们在<div> 标签上添加了ref="demoref",这是 Vue 的基本语法,鉴于有了 TS 的加持下,可以轻松的获取到标签的DOM结构,之后,我们用 log 打印一下该 DOM 结构

Tips小提示:ref<HTMLElement>()开发当中加入TS规范有助于代码提示

<template>
  <div class="" ref="demoref">{{ activeNames }}</div>
</template>
<script setup lang="ts">
const activeNames = ref(1);
const increment = async () => {};
const demoref = ref<HTMLElement>();
// DOM还未更新
console.log(demoref.value);
  // 异步微任务
nextTick(() => {
  // DOM更新了
  console.log("NextTick: ", demoref.value);
});
</script>
<style scoped lang="scss"></style>

在 macrotask 中复现

nextTick(() => {
  // DOM更新了
  console.log("NextTick: ", demoref.value);
});
setTimeout(() => {
  // 宏任务队列中可以拿到更新后的DOM结构数据
  console.log("macortask: ", demoref.value);
}, 1000);

问题一:在 vue 中为什么不能直接拿到 DOM 结构?

出自于 VUE 的生命周期和渲染机制的原因,组件的渲染过程是异步的,需要等待组件渲染完成后才可以获取 DOM 结构数据,以下几点:

[1] Vue的生命周期:mounted()钩子是组件挂在到 DOM 上之后调用的,这时才能确保 DOM 已经加载完成。

[2] 虚拟DOM(简称 VDOM)的异步更新: 用虚拟 DOM 进行 DOM 更新,意味着组件的 DOM 结构不是即刻更新的而是通过异步更新方式进行更新,so~, 需要等待更新完成后才能获取到最新的 DOM 结构。

[3] 数据驱动视图: Vue 是数据驱动的框架,当数据发生改变时,会重新渲染视图,意味着在数据更新后才能获取更新后的 DOM 结构。

Vue 整个生命周期

V3 生命周期列表

执行顺序名称作用
1onBeforeMount()在组件挂载到 DOM 之前执行
2onMounted()在组件挂载到 DOM 之后执行
3onBeforeUpdate()在响应式数据发生变化,且组件重新渲染前执行
4onUpdated()在组件重新渲染并更新 DOM 之后执行
5onBeforeUnmount()在组件卸载之前执行
6onUnmounted()在组件卸载之后执行

示例

<div class="" ref="demoref">{{ activeNames }}</div>

        <el-button @click="increment()">更新DOM</el-button>
<script setup lang="ts">
onUpdated(() => {
  console.log("onUpdated: ", demoref.value);
});

onMounted(() => {
  console.log("onMounted: ", demoref.value);

  // 挂载完成后 模拟响应式数据更新
  activeNames.value = 33;
});

onBeforeUpdate(() => {
  console.log("onBeforeUpdate: ", demoref.value);
});

onUnmounted(() => {
  console.log("onUnmounted: ", demoref.value);
});

onBeforeMount(() => {
  console.log("onBeforeMount: ", demoref.value);
});

onBeforeUnmount(() => {
  console.log("onBeforeUnmount: ", demoref.value);
});

nextTick(() => {
  console.log("NextTick: ", demoref.value);
});

setTimeout(() => {
  console.log("macortask: ", demoref.value);
}, 1000);
  <script>

nextTick 原理

源代码 https://github.com/vuejs/vue/blob/main/src/core/util/next-tick.ts

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = trueelse if (
  !isIE &&
  typeof MutationObserver !== 'undefined' &&
  (isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterDatatrue
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = trueelse if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

执行顺序的优先级如下:

  • Promise.resolve().then:微任务
  • MutationObserver:微任务
  • setImmediate:宏任务
  • setTimeout:宏任务

如果调用nextTick()时,会先检查原生Promise 【微任务(Microtask)】 是否可用,如果可用的话,将会优先设置在Microtask Queue中异步执行。

若原生Promise不可用时,调用MutationObserver 【微任务(Microtask)】 (可以监听 DOM 变化的 Web API)加入到异步队列,

若 DOM 的MutationObserver也不可用时,调用setImmediate 【宏任务(Macrotask)】 ,是否可用,它比 setTimeout 更快地执行回调,因为它不受最小延迟时间的限制。

手写一个nextTick()

我们已经明白了nextTick的作用,在dom结构全部渲染完成后执行。而nextTick就是接受一个回调之后执行,我们可以尝试用上文中的MutationObserve做出监听DOM变化的功能。

const useMyNextTick = (func: any) => {
  let app: any = document.getElementById("app");
  var observerOptions = {
    childListtrue,
    attributestrue,
    subtreetrue,
  };

  let observer = new MutationObserver((el) => {
    console.log(el);
    func();
  });
  observer.observe(app, observerOptions);
};

通过document.getElementById('app')去拿到一个DOM结构,然后写一个监听的配置项。 创建一个监听DOM的对象observer。

而当我们监听的这个app有变更时,就会触发里面的回调函数。

这样我们就简单的实现了一个nextTick.

结尾

写文章不易,如果帮助到了小伙伴们,可以给本文点赞收藏评论三连呀。有不懂的地方欢迎到评论区留言,我们及时互动。