阿卡不拉阿卡不拉
Vue3
阿卡的博客
Vue3
阿卡的博客
  • Vue3

    • 快速入门

      • 搭建工程 👌
      • 模板语法
      • 响应式基础
      • 响应式常用 API
      • 计算属性
      • 类与样式绑定
      • 条件和列表渲染
      • 事件处理
      • 表单处理
      • 生命周期
      • 侦听器
      • 组件介绍
      • Props
      • 自定义事件
      • 组件v-model
      • 插槽
      • 前端路由介绍
      • KeepAlive内置组件
      • 状态管理库
      • 组件库介绍
    • 深入本质

      • 虚拟DOM本质
      • 模板的本质
      • 组件树和虚拟DOM树
      • 数据拦截的本质
      • 响应式数据的本质
      • 响应式的本质
      • 响应式和组件渲染
      • 实现响应式系统 1
      • 实现响应式系统 2
      • 图解EFFECT
      • 实现响应式系统 3
      • 手写computed
      • 手写watch
      • 指令的本质
      • 插槽的本质
      • v-model的本质
      • setup 语法标签
      • 组件生命周期
      • keepalive 生命周期
      • keepalive的本质
      • key的本质
    • 细节补充

      • 【Vue】属性透传
      • 【Vue】依赖注入
      • 【Vue】组合式函数 👌
      • 【Vue】自定义指令
      • 【Vue】插件
      • 【Vue】Transition
      • 【Vue】TransitionGroup
      • 【Vue】Teleport
      • 【Vue】异步组件
      • 【Vue】Suspense
      • 【Router】路由模式
      • 【Router】路由零碎知识
      • 【Router】路由匹配语法
      • 【Router】路由组件传参
      • 【Router】内置组件和函数
      • 【Router】导航守卫
      • 【Router】过渡特效
      • 【Router】滚动行为
      • 【Router】动态路由
      • 【State】通信方式总结
      • 【State】Pinia 自定义插件
      • 【场景】封装树形组件
      • 【场景】自定义 ref 实现防抖
      • 【场景】懒加载
      • 【场景】虚拟列表
      • 【场景】虚拟列表优化
      • 【第三方库】VueUse
      • 【第三方库】vuedragable
      • 【第三方库】vue-drag-resize
      • 【第三方库】vue-chartjs
      • 【第三方库】vuelidate
      • 【第三方库】vue3-lazyload
      • 【场景】Websocket 聊天室
      • 【Vite】✨ 认识 Vite👌
      • 【Vite】配置文件 👌
      • 【Vite】✨ 依赖预构建 👌
      • 【Vite】构建生产版本 👌
      • 【Vite】环境变量与模式
      • 【Vite】CLI
      • 【Vite】Vite 插件
  • 笔面试

    • HTML

      • HTML 面试题汇总
      • 文档声明
      • 语义化
      • W3C 标准组织
      • SEO
      • iframe
      • 微格式
      • 替换元素
      • 页面可见性
    • CSS

      • CSS 面试题汇总
      • CSS 单位总结
      • 居中方式总结
      • 隐藏元素方式总结
      • 浮动
      • 定位总结
      • BFC
      • CSS 属性计算过程
      • CSS 层叠继承规则总结
      • @import 指令
      • CSS3 calc 函数
      • CSS3 媒体查询
      • 过渡和动画事件
      • 渐进增强和优雅降级
      • CSS3 变形
      • 渐进式渲染
      • CSS 渲染性能优化
      • 层叠上下文
      • CSS3 遮罩
    • JavaScript

      • JavaScript 面试题汇总
      • ✨ let、var、const 的区别
      • JS中的数据类型
      • 包装类型
      • 数据类型的转换
      • 运算符
      • ✨ 原型链
      • ✨ this 指向
      • ✨ 垃圾回收与内存泄漏
      • ✨ 执行栈和执行上下文
      • ✨ 作用域和作用域链
      • ✨ 闭包
      • DOM 事件的注册和移除
      • DOM 事件的传播机制
      • 阻止事件默认行为
      • 递归
      • ✨ 属性描述符
      • class 和构造函数区别
      • 浮点数精度问题
      • 严格模式
      • ✨ 函数防抖和节流
      • ✨ WeakSet 和 WeakMap
      • ✨ 深浅拷贝
      • 函数柯里化
      • Node 事件循环
      • 尺寸和位置
    • 浏览器

      • 浏览器面试题汇总
      • ✨ 浏览器的渲染流程
      • ✨ 资源提示关键词
      • 浏览器的组成部分
      • IndexedDB
      • ✨ File API
      • ✨ 浏览器缓存
      • ✨ 浏览器跨标签页通信
      • Web Worker
    • 网络

      • 网络面试题汇总
      • 五层网络模型 👌
      • 常见请求方法 👌
      • ✨cookie👌
      • 面试题
      • 加密
      • ✨JWT👌
      • ✨ 同源策略及跨域问题 👌
      • 文件上传
      • ✨ 输入 url 地址之后
      • 文件下载
      • ✨ session
      • ✨ TCP
      • ✨ CSRF 攻击
      • ✨XSS 攻击 👌
      • ✨ 网络性能优化
      • 断点续传
      • 域名和 DNS
      • SSL、TLS、HTTPS 的关系
      • ✨ HTTP 各版本差异 👌
      • HTTP 缓存协议
      • ✨ WebSocket
    • 工程化

      • CMJ 和 ESM
      • npx
      • ESLint
    • Vue2

      • Vue 面试题汇总相关
      • 组件通信方式总结
      • 虚拟 DOM
      • v-model
      • 数据响应式原理
      • diff
      • 生命周期详解
      • computed
      • 过滤器
      • 作用域插槽
      • 过度和动画
      • 优化
      • keep-alive
      • 长列表优化
      • 其他 API
      • 模式和环境变量
      • 更多配置
      • 更多命令
      • 嵌套路由
      • 路由切换动画
    • Vue3

      • ✨ Vue3 整体变化 👌
      • ✨ Vue3 响应式变化 👌
      • ✨ nextTick 实现原理 👌
      • 两道代码题 👌
      • Vue 运行机制
      • 渲染器核心功能
      • 事件绑定与更新

✨ nextTick 实现原理 👌

面试题:Vue 的 nextTick 是如何实现的?

<template>
    <div>
        <p id="counter" ref="counterRef">{{ count }}</p>
        <button @click="increment">增加计数</button>
    </div>
</template>

<script setup>
import { ref } from "vue";

const count = ref(0);

const increment = () => {
    for (let i = 1; i <= 1000; i++) {
        count.value = i;
    }
};
</script>

思考 🤔:点击按钮后,页面会渲染几次?

答案:只会渲染一次,同步代码中多次对响应式数据做了修改,多次修改会被合并为一次,之后根据最终的修改结果异步的去更新 DOM.

思考 🤔:倘若不合并,并且同步的去修改 DOM,会有什么样的问题?

答案:如果不进行合并,并且数据一变就同步更新 DOM,会导致频繁的重绘和重排,这非常耗费性能。

思考 🤔:异步更新会带来问题

答案:无法及时获取到更新后的 DOM 值

原因:因为获取 DOM 数据是同步代码,DOM 的更新是异步的,同步代码会先于异步代码执行。

解决方案:将获取 DOM 数据的同步任务包装成一个微任务,浏览器在完成一次渲染后,就会立即执行微任务。

当前我们自己的解决方案:

import { ref, nextTick } from "vue";

const count = ref(0);
const counterRef = ref(1);

const increment = () => {
    count.value++;

    Promise.resolve().then(() => {
        console.log("最新的数据:", count.value);
        console.log("通过DOM拿textContent数据:", counterRef.value.textContent);
        console.log(
            "通过DOM拿textContent数据:",
            document.getElementById("counter").textContent
        );
        console.log("通过DOM拿innerHTML数据:", counterRef.value.innerHTML);
        console.log(
            "通过DOM拿innerHTML数据:",
            document.getElementById("counter").innerHTML
        );
    });
};

nextTick 帮我们做的就是上面的事情,将一个任务包装成一个微任务。

const increment = () => {
    count.value++;

    nextTick(() => {
        console.log("最新的数据:", count.value);
        console.log("通过DOM拿textContent数据:", counterRef.value.textContent);
        console.log(
            "通过DOM拿textContent数据:",
            document.getElementById("counter").textContent
        );
        console.log("通过DOM拿innerHTML数据:", counterRef.value.innerHTML);
        console.log(
            "通过DOM拿innerHTML数据:",
            document.getElementById("counter").innerHTML
        );
    });
};

nextTick 返回的是一个 Promise

const increment = async () => {
    count.value++;

    await nextTick();
    console.log("最新的数据:", count.value);
    console.log("通过DOM拿textContent数据:", counterRef.value.textContent);
    console.log(
        "通过DOM拿textContent数据:",
        document.getElementById("counter").textContent
    );
    console.log("通过DOM拿innerHTML数据:", counterRef.value.innerHTML);
    console.log(
        "通过DOM拿innerHTML数据:",
        document.getElementById("counter").innerHTML
    );
};

$nextTick,首先这是一个方法,是 Vue 组件实例的方法,用于 OptionsAPI 风格的。

export default {
    data() {
        return {
            count: 1,
            counterRef: null,
        };
    },
    methods: {
        increment() {
            this.count++;
            this.$nextTick(() => {
                // 在下一个 DOM 更新循环后执行的回调函数
                console.log("最新数据为:", this.count);
                console.log("拿到的DOM:", document.getElementById("counter"));
                console.log("拿到的DOM:", this.$refs.counterRef);
                console.log(
                    "通过DOM拿数据:",
                    document.getElementById("counter").textContent
                );
                console.log(
                    "通过DOM拿数据:",
                    document.getElementById("counter").innerHTML
                );
                console.log(
                    "通过DOM拿数据:",
                    this.$refs.counterRef.textContent
                );
                console.log("通过DOM拿数据:", this.$refs.counterRef.innerHTML);
            });
        },
    },
};

nextTick 源码

// 创建一个已经解析的 Promise 对象,这个 Promise 会立即被解决,
// 用于创建一个微任务(microtask)。
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>

// 一个全局变量,用于跟踪当前的刷新 Promise。
// 初始状态为 null,表示当前没有刷新任务。
let currentFlushPromise: Promise<void> | null = null

// queueFlush 函数负责将刷新任务(flushJobs)放入微任务队列。
// 这是 Vue 的异步更新机制的核心部分,用于优化性能。
function queueFlush() {
  // 检查是否已经在刷新(isFlushing)或者刷新任务是否已被挂起(isFlushPending)。
  if (!isFlushing && !isFlushPending) {
    // 设置 isFlushPending 为 true,表示刷新任务已被挂起,正在等待执行。
    isFlushPending = true
    // 将 currentFlushPromise 设置为 resolvedPromise.then(flushJobs)
    // 这将创建一个微任务,当 resolvedPromise 被解决时,执行 flushJobs 函数。
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}

// nextTick 函数用于在下一个 DOM 更新循环之后执行一个回调函数。
// 它返回一个 Promise,这个 Promise 会在 DOM 更新完成后解决。
export function nextTick<T = void, R = void>(
  this: T,
  fn?: (this: T) => R,  // 可选的回调函数,在 DOM 更新之后执行
): Promise<Awaited<R>> {
  // 如果 currentFlushPromise 不为 null,使用它;否则使用 resolvedPromise。
  // 这样可以确保在 DOM 更新之后再执行回调。
  const p = currentFlushPromise || resolvedPromise

  // 如果传入了回调函数 fn,返回一个新的 Promise,在 p 解决之后执行 fn。
  // 使用 this 绑定来确保回调函数的上下文正确。
  return fn ? p.then(this ? fn.bind(this) : fn) : p
  // 如果没有传入回调函数 fn,直接返回 Promise p,这样外部代码可以使用 await 等待 DOM 更新完成。
}

面试题

Vue 的 nextTick 是如何实现的?

参考答案:

nextTick 的本质将回调函数包装为一个微任务放入到微任务队列,这样浏览器在完成渲染任务后会优先执行微任务。

nextTick 在 Vue2 和 Vue3 里的实现有一些不同:

  1. Vue2 为了兼容旧浏览器,会根据不同的环境选择不同包装策略:
  • 优先使用 Promise,因为它是现代浏览器中最有效的微任务实现。

  • 如果不支持 Promise,则使用 MutationObserver,这是另一种微任务机制。

  • 在 IE 环境下,使用 setImmediate,这是一种表现接近微任务的宏任务。

  • 最后是 setTimeout(fn, 0) 作为兜底方案,这是一个宏任务,但会在下一个事件循环中尽快执行。

  1. Vue3 则是只考虑现代浏览器环境,直接使用 Promise 来实现微任务的包装,这样做的好处在于代码更加简洁,性能更高,因为不需要处理多种环境的兼容性问题。

整体来讲,Vue3 的 nextTick 实现更加简洁和高效,是基于现代浏览器环境的优化版本,而 Vue2 则为了兼容性考虑,实现层面存在更多的兼容性代码。

最近更新:: 2025/7/16 12:57
Contributors: AK
Prev
✨ Vue3 响应式变化 👌
Next
两道代码题 👌