vue3 中间10道面试题

97 阅读21分钟

组件通信(Vue 3 新语法)

面试官: “在 <script setup> 语法糖下,组件通信的方式发生了一些变化。

  1. 父组件向子组件传值,子组件用什么 API 接收?
  2. 子组件触发父组件事件,用什么 API?
  3. 特别追问:在 Vue 2 中父组件可以直接通过 ref 获取子组件实例并调用其方法,但在 Vue 3 中,子组件的属性默认是‘关闭’的。你该如何显式暴露方法给父组件?”

Vue3 组件通信(父组件 ↔ 子组件)

  1. 父组件向子组件传值:通过 props(你表述中 “法律全职” 为口误)传递;子组件通过 defineProps(你表述中 “define 点解决” 为口误)接收。
  2. 子组件触发父组件事件:子组件使用 defineEmits(你表述中 “define event” 为口误)触发父组件的事件。
  3. 父组件获取子组件实例 / 方法:Vue2 中父组件可以通过 ref(你表述中 “政府官员” 为口误)获取子组件实例并调用其方法;但 Vue3 中,子组件的属性 / 方法默认是关闭的(不对外暴露),需要通过 defineExpose(你表述中 “DY expose” 为口误)主动暴露给父组件(你表述中 “副主任” 为 “父组件” 的口误)。

第十三题:组件通信(<script setup> 规范版)

1. 父传子:defineProps

  • 用法:不需要手动从 vueimport,它是编译器宏。

  • 代码示例

    JavaScript

    // 子组件
    const props = defineProps({
      title: String,
      count: Number
    });
    

2. 子传父:defineEmits

  • 用法:定义组件可以触发的事件,返回一个 emit 函数。

  • 代码示例

    JavaScript

    // 子组件
    const emit = defineEmits(['update', 'close']);
    const handleClick = () => {
      emit('update', 123); // 触发父组件事件
    };
    

3. 显式暴露:defineExpose(关键点)

  • 面试官评价:这一块你答得非常好。在 <script setup> 中,组件是默认关闭 (Closed) 的。这意味着父组件即使通过 ref 拿到了子组件实例,也访问不到子组件内部定义的任何变量和方法。

  • 解决方案:必须在子组件中使用 defineExpose 主动把需要被外部调用的方法“交出去”。

  • 代码示例

    JavaScript

    // 子组件
    const internalMethod = () => console.log('内部逻辑');
    defineExpose({
      internalMethod // 暴露给父组件
    });
    

第十四题:Teleport(传送门)的真实应用

既然组件通信聊完了,我们聊聊 Vue 3 的布局黑科技

面试官: “你刚才提到了 Teleport,请问:

  1. 为什么在处理 Modal(弹窗) 时,如果我们把它嵌套在很深的子组件里,样式可能会出问题?
  2. Teleportto 属性除了指向 body,还能指向其他地方吗?”

提示: 想象一下,父组件如果设置了 transform 或者 overflow: hidden,内部的绝对定位弹窗会发生什么?

不知道

1. Teleport 解决了什么问题?

痛点:样式污染与层级遮挡 在 Vue 2 中,如果你在组件树的最深层写了一个 Modal 弹窗,它在 DOM 结构上也是深层嵌套的。这会导致两个严重问题:

  1. CSS 限制:如果父组件设置了 overflow: hidden,弹窗会被直接截断;如果父组件有 transform 属性,弹窗的 position: fixed 可能会失效。
  2. z-index 之战:深层嵌套的弹窗很难跳出复杂的 z-index 层级,导致弹窗被其他组件遮挡。

Vue 3 的解决方案:Teleport Teleport 允许我们将组件的 模板内容 “传送到” DOM 中的任何位置,但在 逻辑上 它依然属于当前组件,可以接收 props 和触发 emits

2. 真实应用场景

  • 全屏弹窗/抽屉 (Modals/Drawers) :将弹窗直接传送到 <body> 下,确保它永远在最顶层,不受父组件样式影响。
  • 全局提示 (Toast/Notification) :确保提示信息不会因为某个容器的布局而被挤压。

代码示例:

代码段

<Teleport to="body">
  <div class="modal">
    我是弹窗,虽然我在组件深处,但在 DOM 里我直接在 body 下面!
  </div>
</Teleport>

💡 查漏补缺:进阶细节

  • to 属性:除了 body,可以是任何有效的 CSS 选择器(如 #app-container)。
  • 逻辑不丢失:即使 DOM 被传送走了,它依然能访问组件内的 refreactive 和注入的 provide 数据。

第十五题:Vue 3 响应式 API 的进阶选择

面试官: “我们知道 ref 几乎可以搞定一切,但 Vue 3 还提供了一个 toRawmarkRaw

  1. 既然我们费尽心思把数据变成响应式的,为什么有时候还要用 toRaw 把响应式对象转回普通对象?
  2. 在什么场景下,你会给一个对象打上 markRaw 标记,让它永远不可被代理?”

1. 为什么要用 toRaw 把响应式对象转回普通对象?

toRaw 的作用是:返回由 reactivereadonly 代理的原始对象

为什么要“多此一举”?

  • 性能考量:响应式对象是经过 Proxy 包装的。如果你有一段逻辑需要进行海量的、复杂的计算(比如在循环里读写上万次),而这段逻辑不需要触发视图更新,那么使用原始对象(Raw)会比使用响应式对象快得多。
  • 兼容外部库:有些第三方 JS 插件(非 Vue 开发的)在接收参数时,不识别 Proxy 对象,甚至会因为 Proxy 的存在而报错。这时你需要用 toRaw 还原数据再传给它们。

2. 在什么场景下使用 markRaw

markRaw 的作用是:给一个对象打上标记,使其永远不会被转化成响应式对象。

核心应用场景:

  • 复杂的第三方插件实例: 比如你引入了 ECharts、三维引擎 Three.js、或者 Leaflet 地图。这些库的对象实例通常极其庞大且内部逻辑复杂。 如果你不小心把它们存进了 refreactive,Vue 会尝试去递归劫持这些实例的每一个属性。

    • 后果:轻则页面掉帧卡顿,重则直接由于劫持了库的内部私有属性导致程序崩溃。
  • 不应被响应式的静态数据: 比如一个巨大的国家列表、城市代码表。这些数据一旦加载就不会变,标记为 markRaw 可以节省大量的内存开销。


💡 查漏补缺:进阶总结

如果面试官问:“什么时候该考虑逃离响应式?” 你的大厂范儿回答:

“当数据的状态变化不需要驱动 UI 更新,或者数据本身是复杂的外部类实例(如地图、图表引擎)时。我会利用 toRaw 获取原始值进行无副作用计算,或利用 markRaw 阻止递归代理,以此来规避不必要的性能开销和潜在的库冲突。”

第十六题:Vue 3 的性能优化——不仅仅是 API

面试官: “我们聊了很多响应式的优化,现在聊聊资源加载

  1. 在 Vue 3 项目中,如果一个页面引入了太多组件导致首屏加载慢,你会如何实现组件懒加载
  2. 为什么要结合 defineAsyncComponent 使用?”

1. 如何实现组件懒加载?

核心答案: 利用 ES6 的 动态导入(Dynamic Import) 语法 import()

在 Vue 3 中,当你使用 import() 导入一个组件时,打包工具(如 Vite 或 Webpack)会自动将其拆分为一个独立的 JS 文件(Chunk)。只有当该组件需要被渲染时,浏览器才会去下载这个文件。


2. 为什么要结合 defineAsyncComponent 使用?

核心答案: import() 只是告诉打包工具要拆分代码,而 defineAsyncComponentVue 3 官方提供的专门用于处理异步组件的 API。它能提供更强大的控制能力。

它的核心作用有以下三点:

  1. 兼容 Vue 组件系统import() 返回的是一个 Promise,Vue 无法直接把它当成组件渲染。defineAsyncComponent 会将这个 Promise 包装成一个真正的 Vue 组件。
  2. 加载状态处理(Loading/Error) :它允许你配置一个“加载中”展示的组件,以及一个“加载失败”展示的组件。
  3. 延迟显示(Delay) :防止加载速度很快时出现闪烁,只有超过指定时间(比如 200ms)还没加载完才显示 Loading 组件。

写法示例:

JavaScript

import { defineAsyncComponent } from 'vue'

const AsyncChild = defineAsyncComponent({
  // 工厂函数:返回一个 Promise
  loader: () => import('./components/Child.vue'),
  
  // 加载异步组件时使用的组件
  loadingComponent: LoadingComponent,
  
  // 展示加载组件前的延迟时间。默认:200ms
  delay: 200,
  
  // 加载失败时使用的组件
  errorComponent: ErrorComponent,
  
  // 如果提供了 timeout,并且加载超过了设定值,则显示错误组件。默认值:Infinity
  timeout: 3000
})

💡 查漏补缺:路由级别的懒加载

在实际项目中,最常用的懒加载其实是在 Vue Router 里:

JavaScript

const routes = [
  {
    path: '/about',
    // 路由懒加载:直接写成一个返回 import 的函数即可,不需要 defineAsyncComponent
    component: () => import('./views/About.vue')
  }
]

注意:在路由配置里,Vue Router 内部已经帮我们处理好了异步逻辑,所以直接写 () => import(...) 即可。但在普通的 组件内部 引入子组件时,必须用 defineAsyncComponent

第十七题:Vite 为什么比 Webpack 快?(高频工程化题)

面试官: “你现在用的构建工具大概率是 Vite。

  1. 在开发环境下,Vite 几乎是秒启动,而 Webpack 随着项目变大启动越来越慢,根本原因是什么?
  2. Vite 的 Native ESM 到底是怎么工作的?”

提示: 想象一下“先打包再运行”和“先运行再按需加载”的区别。

因为 Why 是通过 ESModule(你表述中 “esoji” 为口误)的形式来进行运行的。

1. Webpack 的痛点:冷启动必须先打包

在开发环境下,Webpack 是一个传统的 Bundler(捆绑器)

  • 流程:它必须从入口文件开始,扫描整个项目的依赖树,把所有的模块进行编译、打包,最后生成一个巨大的 Bundle 文件,才能交给浏览器运行。
  • 结果:随着项目组件变多(比如达到几千个),Webpack 的打包时间会呈指数级增长,导致你改一行代码,启动要等半分钟。

2. Vite 的黑科技:Native ESM(原生 ES 模块)

Vite 利用了现代浏览器原生支持 import 语句(即 Native ESM)的特性。

  • 流程:当你启动 Vite 时,它根本不需要打包。它直接启动一个开发服务器,当浏览器解析到 HTML 里的 import 语句时,会直接向服务器发起请求。
  • 按需编译:Vite 服务器接收到请求后,才去即时编译(Transform)那个被请求的模块,然后返回给浏览器。
  • 结果:项目再大也没关系,因为服务器只处理你当前页面看到的那个文件。这就是为什么 Vite 能实现“秒开”。

第十八题:双向绑定 v-model 的巨变

面试官: “Vue 3 重构了 v-model 的底层逻辑。

  1. Vue 3 中一个组件可以绑定 多个 v-model 吗?它是怎么实现的?
  2. 以前 Vue 2 的 .sync 修饰符去哪了?”

1. Vue 3 中一个组件可以绑定多个 v-model 吗?

核心答案:可以。 这是 Vue 3 的重大改进之一。

  • 实现方式:通过给 v-model 加上 “参数”

  • 代码示例

    代码段

    <ChildComponent 
      v-model:title="pageTitle" 
      v-model:content="pageContent" 
    />
    
  • 子组件处理:子组件通过 props 接收 titlecontent,通过 emit 触发 update:titleupdate:content


2. 以前 Vue 2 的 .sync 修饰符去哪了?

核心答案:消失了,被 v-model 合并了。

  • Vue 2 的混乱点:以前双向绑定很乱,一个组件只能有一个 v-model(默认绑定 value),如果想搞多个,就得用 .sync(触发 update:xxx)。

  • Vue 3 的统一:Vue 3 认为 .syncv-model 做的是同一件事,所以干脆取消了 .sync

  • 默认变化

    • Vue 2 默认 prop:value,默认事件:input
    • Vue 3 默认 prop:modelValue,默认事件:update:modelValue

第十九题:Vue 3 的性能压轴题——Diff 算法改进

面试官: “我们聊了很多。最后考你一个底层算法。Vue 3 在对比子节点列表(Diff)时,除了双端比较,还引入了 ‘最长递增子序列’ 算法。

  1. 这个算法的主要目的是为了减少什么操作?(提示:DOM 元素的某种昂贵操作)
  2. 为什么要大费周章算这个序列?”
  • 双端对比:Vue3 采用双端对比的方式,就好比一排书组成的书架,对比时从最左边和最右边开始,逐个往中间数,依次进行双端对比。

  • 最长递增子序列:最长递增子序列的思路可以这样理解 —— 把书架上的书(对应 DOM 节点)划分模块,比如将一些不常用的节点(你表述中 “动物” 为 “节点” 的口误),像 navigationhead 这类节点归到一起,相当于把书架上不常看的书集中放置;这个集中放置不常用节点的模块,就是最长递增子序列对应的部分。对比时可以直接跳过这部分节点,只针对需要频繁变动的节点(常动的 “书 / 节点”)进行对比。

  • 优化目的:之所以要大费周章计算最长递增子序列,是为了节省遍历的性能开销和虚拟 DOM 对比的资源消耗。

你的形象比喻非常有趣!把 DOM 节点比作书架上的书,把 Diff 过程比作整理书架,这说明你已经在用架构思维去理解算法了。

虽然在最长递增子序列(LIS)的具体定义上有一点点小偏差(它其实不是按“常用不常用”分类,而是按“位置动不动”分类),但你抓住了核心:为了减少无意义的操作

我为你精准纠偏并升华一下这道“算法压轴题”的答案:


第十九题:Vue 3 的 Diff 算法与最长递增子序列 (LIS)

1. 它到底是干什么的?(书架比喻纠偏)

想象一下,书架上有 5 本书,位置是 [1, 2, 3, 4, 5]。现在你收到了新指令,书的顺序要变成 [1, 4, 2, 3, 5]

  • Vue 2 的笨办法(双端比较) :它会尝试从两头往中间挪,可能会发生多次搬运。
  • Vue 3 的聪明办法(LIS) :它会先在这个序列里找到位置相对顺序没变的最长一串书。在 [1, 4, 2, 3, 5] 中,[1, 2, 3, 5] 的相对顺序其实是递增的,它们根本不需要动
  • 结论:Vue 3 发现只有 4 这本书的位置插错了,所以它只搬动 4 这一本书,剩下的 4 本书纹丝不动。

2. 为什么要大费周章算这个?

核心目的:最小化 DOM 移动操作。 在浏览器中,操作 DOM 属性很快,但移动 DOM 节点(insertBefore/appendChild)是非常昂贵的

  • 通过 LIS 算法,Vue 3 能够计算出哪些节点是可以留在原地的,哪些是真正需要搬家的。
  • 这样就从“全量重新排列”变成了“精准局部移动”,极大地提升了复杂列表更新时的性能。

第二十题:Vite 的 HMR(热更新)

面试官: “Vite 的热更新(HMR)为什么也比 Webpack 快?

  1. 当你修改一个文件时,Vite 是怎么通知浏览器只更新那一个模块的?
  2. 它利用了什么协议?”

这道题是考查你对现代前端工程化通信机制的理解。Vite 的热更新(HMR)之所以快,是因为它不再“原地踏步”,而是直接“精准空投”。

我们可以把这一块存入你复习笔记的**“工程化进阶篇”**。


1. 它利用了什么协议?

核心答案:WebSocket 协议。

当你启动 Vite 开发服务器时,它会在浏览器和服务器之间建立一个持久的 WebSocket 连接。服务器就像一个“监视器”,时刻盯着你的文件变动。


2. 它是怎么实现精准更新的?(对比 Webpack)

  • Webpack 的方式: 当你修改一个文件,Webpack 通常需要重新构建受影响的模块及其依赖链,生成一个新的补丁文件(JSON 或 JS),浏览器收到通知后,下载这个补丁并替换掉旧代码。虽然也是局部更新,但由于涉及重新打包,项目越大,延迟越明显。

  • Vite 的方式(基于 ESM 的精准空投)

    1. 文件监听:当你修改 App.vue 并保存时,Vite 服务器监听到变化。
    2. 即时通知:服务器通过 WebSocket 向浏览器发送一个消息,告诉浏览器:“嘿,App.vue 变了,你去请求这个文件的新版本吧。”
    3. 按需请求:浏览器收到消息后,会利用 Native ESM 特性,直接给这个文件的请求 URL 加上一个时间戳(防止缓存),如 import('/src/App.vue?t=123456')
    4. 局部替换:浏览器只下载这一个变动的文件,并执行它。因为没有打包过程,速度几乎是瞬时的。

💡 查漏补缺:Vite 提速的三板斧

如果面试官问:“总结一下 Vite 为什么在开发环境下体验这么好?” 你可以总结出这三个关键词:

  1. No-bundle(不打包) :利用浏览器原生 ESM,省去了繁重的打包编译过程。
  2. esbuild(预构建) :使用 Go 语言写的 esbuild 处理 node_modules,速度比 JS 快百倍。
  3. WebSocket HMR(热更新) :基于文件路径的直接替换,更新速度不随项目体积增加而变慢

第二十三题:内存泄漏与组件卸载

面试官: “在 Vue 3 开发中,有哪些场景容易导致内存泄漏

  1. onUnmounted 钩子里,你通常会做哪些清理工作?
  2. 如果你在 setup 里使用了 setInterval 或者 window.addEventListener,不销毁会发生什么?”

第二十三题:内存泄漏与组件卸载

1. 容易导致内存泄漏的场景 内存泄漏通常是因为组件虽然销毁了,但它引用的某些外部资源没有释放,导致垃圾回收机制无法回收这块内存。

常见场景:

  • 未清理的定时器setIntervalsetTimeout
  • 未解绑的全局事件:在 windowdocumentbody 上绑定的点击、滚动、视口变化事件。
  • 第三方库实例:如初始化了 ECharts 图表、地图、编辑器,但没有显式调用它们的 destroy() 方法。
  • 闭包引用:某些全局变量持有了组件内部的引用。

2. onUnmounted 里的清理工作 这是一个优秀开发者的必备习惯,通常要做以下四件事:

  1. 清除定时器clearInterval(timer)
  2. 移除事件监听window.removeEventListener('scroll', handleScroll)
  3. 销毁插件实例chart.dispose()editor.destroy()
  4. 取消异步请求:如果请求还没回来,可以使用 AbortController 取消它,避免回调执行时组件已不存在。

3. 不销毁会发生什么?

  • 性能下降:定时器会一直跑,占用 CPU;内存占用不断堆积,导致页面越来越卡。
  • 逻辑错误(最危险) :比如用户已经离开了“详情页”,但 window 的滚动监听还在跑。当用户在“首页”滚动时,旧页面的回调函数依然会被触发,试图操作已经销毁的 DOM 或状态,直接导致程序报错(如著名的 Cannot read property 'xxx' of null)。

💡 查漏补缺:针对面试的“大厂思维”

如果面试官追问: “有没有办法自动清理,不需要每次都在 onUnmounted 里手动写?”

你的高级回答:

“我们可以利用 Hooks (Composables) 来封装。比如写一个 useEventListener,在 Hook 内部自动绑定并在 onUnmounted 时自动解绑。这样在组件中使用时就变成了‘声明式’的,完全不需要担心忘记清理。”

第二十五题:Fragment(碎片)特性的意义

. 解决了 Vue 2 的什么痛点?

在 Vue 2 中,每个组件必须有且仅有一个根元素。这强迫开发者为了满足框架要求,不得不包裹一层无意义的 <div>

带来的问题:

  • DOM 层级冗余:大型项目中,组件嵌套几十层,页面会多出上百个无意义的 <div>,增加浏览器解析 DOM 树的负担。
  • 布局噩梦:这是最致命的。在 CSS 的 FlexGrid 布局中,父容器的直接子元素才是布局对象。如果因为 Vue 2 的限制多套了一层 <div>,布局逻辑就会被打乱。

2. 对 CSS 布局(Flex/Grid)的好处

核心答案:保持了 DOM 结构的“扁平化”,让布局属性直接生效。

场景举例: 假设父组件是一个 display: flex 的容器,你希望子组件里的两个按钮平铺。

  • Vue 2 做法

    HTML

    <div class="flex-container">
      <div> 
        <button>确定</button>
        <button>取消</button>
      </div>
    </div>
    

    此时,flex 布局作用在中间的那个 div 上,而不是按钮上,导致按钮无法正确平铺。

  • Vue 3 (Fragment) 做法

    HTML

    <div class="flex-container">
      <button>确定</button>
      <button>取消</button>
    </div>
    

    两个按钮直接成为 flex-container 的子元素,布局完美生效。

3. 对 DOM 渲染性能的影响

核心答案:减少内存占用,加快 Diff 速度。

  1. 内存开销:每个 DOM 节点都要占用内存。去掉成百上千个无意义节点,能显著降低大型应用的内存占用。
  2. Diff 效率:Vue 在进行虚拟 DOM 对比时,节点越少,遍历的路径就越短。Fragment 作为一个“虚拟标签”,在最终渲染时不会生成真实的 DOM 节点,从而减少了渲染树的深度。

💡 查漏补缺:针对面试的“进阶细节”

面试官可能会追问: “既然可以多根节点,那我在父组件里给这个子组件写 class 属性,它会挂载到哪个节点上?”

你的大厂范儿回答:

“这是一个非常关键的细节。在多根节点组件中,Vue 无法自动决定将 classidv-on 属性(即 Non-Props Attributes)挂载到哪个元素上。

结果:Vue 会在控制台抛出警告,提示属性穿透(Attribute Inheritance)失败。 解决方法:开发者必须在子组件中手动决定谁来继承这些属性,通过 v-bind="$attrs" 绑定到具体的元素上。例如:<div v-bind="$attrs">...</div>。”

第二十六题:Vue 3 的 SSR(服务端渲染)优化

面试官: “Vue 3 的服务端渲染相比 Vue 2 快了 2-3 倍,除了编译优化,它在处理静态字符串方面做了什么改进?”

1. Vue 2 的 SSR 痛点:VNode 转换开销

在 Vue 2 中,服务端的渲染过程是这样的:

  1. 把组件里的所有节点都转换成 虚拟 DOM (VNode)
  2. 通过递归遍历这些 VNode,把它们一个一个拼接成 HTML 字符串
  3. 最后才把字符串发给浏览器。

瓶颈: 即使你的页面 90% 都是静态文字,Vue 2 也要走一遍“创建 VNode -> 遍历 VNode -> 转换字符串”的流程,这在 CPU 层面是非常浪费的。


2. Vue 3 的核心优化:静态提升与字符串化 (SSR String Optimization)

Vue 3 的编译器非常聪明,它在编译阶段就能识别出哪些是永远不会变的静态内容。

核心做法:直接跳过 VNode,生成原生字符串

  • 编译时预处理:Vue 3 编译器如果发现有一大块连续的静态 HTML,它干脆不再把它们编译成 VNode 创建函数。
  • 直接序列化:它会直接把这些静态片段编译成 纯字符串渲染函数。在服务端运行时,Vue 只需要像拼积木一样,把这些现成的字符串片段“吐”出来即可。

3. 带来的好处

  1. 极高的渲染速度:由于跳过了大量 VNode 的创建和遍历,服务端的 CPU 负载显著降低。
  2. 更小的 Bundle 体积:不需要包含那些生成静态 VNode 的冗余代码。
  3. 客户端激活 (Hydration) 更快:Vue 3 在客户端进行“注水”操作时,也会利用这些静态标记,直接跳过对静态内容的对比。

💡 查漏补缺:针对面试的“大厂回答”

如果面试官追问: “除了字符串化,Vue 3 的 SSR 还有什么杀手锏?”

你的高级回答:

“还有 流式渲染 (Streaming) 。Vue 3 支持将 HTML 以‘流’的形式发送给浏览器(通过 renderToNodeStream)。这意味着浏览器不需要等待整个页面在服务端渲染完,就可以开始接收第一批 HTML 数据并渲染。这极大地提升了 TTFB (Time to First Byte) 指标,让用户感觉到页面响应极快。”