跳到主要内容

Vue 热门面试题

使用说明

本篇共 60 道题。

Vue 面试重点是响应式依赖、组件更新、Composition API 和工程取舍。口头回答先说机制,再补使用建议和常见陷阱。

Q1: Vue 3 为什么使用 Proxy?

答案

Vue 2 的 Object.defineProperty 只能拦截已经定义到具体属性上的 getter/setter。新增或删除属性、数组索引和长度变化需要额外约定,初始化时还要递归遍历对象。

Proxy 包装整个对象,可以统一拦截 getsetdeletePropertyhasownKeys 等操作,因此 Vue 3 能自然观察属性增删、数组和集合类型,并在访问嵌套对象时按需创建代理。

代价是 Proxy 无法为不支持它的旧环境完整 polyfill;此外只有通过代理对象进行的操作才会被追踪。Vue 仍需用 WeakMap 缓存代理,并通过 toRawmarkRaw、浅层 API 处理特殊对象和性能边界。

Q2: Vue 3 响应式系统大致如何工作?

答案

读取响应式属性时收集当前活跃副作用与“目标+键”的依赖关系,写入变化时找到相关副作用并调度执行。

Vue 会通过调度队列批量刷新组件和 watcher,避免一次同步流程中重复更新。没有被读取的属性不会凭空建立依赖。

  • 读取响应式属性时 track 当前副作用,写入时 trigger 相关副作用;依赖通常按 target、key 分层存储,避免无关更新。
  • computed 在此之上增加缓存与脏标记,组件渲染也是一种响应式副作用。实际调试要关注意外解构、重复代理和过大的依赖范围。

Q3: refreactive 怎么选?

答案

维度refreactive
数据类型任意类型仅对象/数组
访问修改.value直接访问
整体替换✅ 支持❌ 不支持
解构需要 toRefs需要 toRefs
TS 类型推导更好一般

选择建议

  • 基本类型:只能用 ref
  • 对象类型:两者都可以,推荐 ref(更一致的心智模型)
  • 组合式函数返回值:推荐 ref(方便解构)
// 推荐:统一使用 ref
const count = ref(0);
const user = ref({ name: 'Tom' });

// 组合式函数
function useCounter() {
const count = ref(0);
const increment = () => count.value++;
return { count, increment }; // 返回 ref,调用者可直接解构
}

Q4: computedwatch 有什么区别?

答案

场景选择原因
从已有数据派生新数据computed有缓存,自动依赖追踪
需要执行异步操作watchcomputed 不支持异步
需要访问旧值watchcomputed 没有旧值
需要执行 DOM 操作watchcomputed 不应有副作用
数据变化后发请求watch典型的副作用场景
// ✅ computed:派生数据
const fullName = computed(() => `${firstName.value} ${lastName.value}`);

// ✅ watch:副作用
watch(userId, async (id) => {
userData.value = await fetchUser(id);
});

// ❌ 错误:computed 执行副作用
const data = computed(() => {
fetch('/api/data'); // 不要这样做!
return someValue.value;
});

Q5: watchwatchEffect 怎么选?

答案

const count = ref(0);
const name = ref('Tom');

// watch:显式声明依赖
watch(count, (newVal, oldVal) => {
console.log('count changed:', oldVal, '→', newVal);
});

// watchEffect:自动追踪
watchEffect(() => {
// 自动追踪 count 和 name
console.log(count.value, name.value);
});
维度watchwatchEffect
依赖声明显式自动推断
执行时机依赖变化时立即执行 + 依赖变化时
旧值访问(newVal, oldVal)
精确控制✅ 只侦听指定源❌ 追踪所有使用的响应式数据

选择建议

  • 需要旧值或精确控制 → watch
  • 依赖复杂或需要立即执行 → watchEffect

Q6: nextTick 的作用是什么?

答案

Vue 会批量调度 DOM 更新,修改响应式状态后同步读取 DOM 可能还是旧结果。nextTick 等待当前这一轮更新提交完成。

它不是通用延时函数。大量依赖 nextTick 操作 DOM 往往说明数据流或组件抽象需要调整。

  • Vue 会把同一轮同步状态变更批量放入微任务队列,DOM 不会在赋值后立刻逐次更新。nextTick 等待这轮渲染刷新完成。
  • 它适合更新后读取尺寸或操作焦点,不应被当作修复数据流问题的“万能延时器”;连续依赖 DOM 的流程更适合封装为指令或组件。

Q7: Vue 的虚拟 DOM 与 Diff 大致如何工作?

答案

组件渲染生成 VNode,更新时比较新旧树,按节点类型和 key 复用或替换,并把必要变化提交到真实 DOM。

Vue 编译器还能标注静态节点和动态部分,让运行时跳过不可能变化的区域。Diff 仍依赖稳定结构和正确 key。

  • Vue 先比较节点类型和 key,再按编译期 PatchFlag 跳过静态内容,只更新动态属性或子节点。
  • 带 key 的列表会同步头尾、建立 key 到索引的映射,并通过最长递增子序列尽量减少 DOM 移动;稳定 key 决定状态能否正确复用。

Q8: v-for 中的 key 为什么重要?

答案

key 帮助 Vue 的 Diff 算法识别节点身份,实现:

  1. 准确复用:通过 key 精确匹配新旧节点
  2. 最小化 DOM 操作:只更新变化的部分
  3. 保持组件状态:确保状态与正确的节点关联
  4. 正确的 Transition 动画:动画应用到正确的元素
// 没有 key:按顺序就地复用(可能复用错误)
// 旧: [A, B, C] -> 新: [B, C, A]
// Vue 认为:A->B(更新), B->C(更新), C->A(更新) = 3 次更新

// 有 key:根据 key 匹配
// Vue 认为:A 移动到末尾 = 1 次移动

Q9: v-ifv-show 怎么选?

答案

维度v-ifv-show
实现方式条件为假不渲染 DOMCSS display: none
切换开销高(DOM 操作)低(CSS 切换)
初始开销低(惰性渲染)高(始终渲染)
生命周期每次切换触发只首次触发

使用建议

  • 频繁切换(Tab、下拉菜单)→ v-show
  • 条件很少改变(权限控制、登录状态)→ v-if
  • 首屏优化(懒加载组件)→ v-if

Q10: Vue 组件有哪些通信方式?

答案

父子用 props 和 emits;跨层但作用域明确可用 provide/inject;共享复杂状态用 Pinia 等 Store;路由和服务端数据各有独立状态来源。

不应为了少写几层 props 就把所有状态全局化。事件表达“发生了什么”,不要让子组件直接修改父组件内部状态。

Q11: Composition API 相比 Options API 有什么价值?

答案

  1. 更好的代码组织:将相关逻辑放在一起,而非分散在不同选项中
  2. 更好的逻辑复用:Composables 替代 Mixins,解决了命名冲突、来源不清等问题
  3. 更好的 TypeScript 支持:不依赖 this,类型推断更准确
  4. 更小的打包体积:变量名可以被压缩(不像 Options API 的属性名)
  5. 更灵活的组合:可以自由组合多个 Composables

Q12: script setup 做了什么?

答案

特性script setup普通 setup()
语法顶层代码即 setup需要在 setup() 函数内
return自动暴露需要手动 return
组件注册自动注册需要 components 选项
props/emits使用编译器宏通过参数获取
性能更好(编译优化)一般
TypeScript更好的类型推断需要 defineComponent

Q13: Vue 生命周期中适合做什么?

答案

setup 阶段建立响应式逻辑;mounted 后才能可靠访问客户端 DOM;updated 不应无条件再次改状态;unmounted 要清理监听、定时器和外部实例。

数据请求放在哪里取决于 SSR、路由和缓存策略,不应简单背“都放 mounted”。

Q14: keep-alive 的原理和风险是什么?

答案

keep-alive 的核心原理:

  1. 缓存 VNode:使用 Map 存储组件的 VNode 和实例
  2. 复用实例:再次渲染时,直接复用缓存的组件实例
  3. LRU 淘汰:设置 max 后,超出限制会删除最久未使用的
  4. 特殊标记:通过 shapeFlag 标记组件为 keep-alive 类型
  5. 生命周期:卸载时不销毁,而是调用 deactivated;恢复时调用 activated

Q15: Teleport 和 Suspense 分别解决什么问题?

答案

Teleport 解决的是DOM 挂载位置问题:组件逻辑仍属于原组件树,但实际 DOM 可以放到 body 或指定容器,适合弹窗、Toast 和全局浮层。

Suspense 解决的是异步依赖就绪前的边界展示问题:默认插槽中的异步 setup 或异步组件尚未完成时显示 fallback,完成后切换到正式内容。

二者职责不同,可以组合使用。Teleport 不会自动实现焦点管理和滚动锁定;Suspense 也不是任意请求的全局 loading 管理器。实际项目还要结合错误边界、超时、路由和 SSR 框架能力设计。

Q16: Vue Router 的前端路由原理是什么?

答案

History 模式通过 History API 修改 URL 并监听导航,Hash 模式使用 # 后片段。Router 根据当前地址匹配记录、执行守卫并渲染对应组件。

History 模式需要服务端把未知前端路由回退到入口 HTML,同时正确处理真正的静态资源和 404。

Q17: Pinia 和 Vuex 的主要差异是什么?

答案

Pinia API 更轻、模块天然、TypeScript 推断更直接,不再强制 mutation 层;Vuex 在旧项目和既有插件生态中仍可能存在。

迁移应关注状态边界、持久化、SSR 隔离和调试,而不是只替换 API 名称。

  • Pinia 以组合式 Store、完整 TypeScript 推导和模块化设计简化了 Vuex 的 mutation/namespace 心智负担。
  • Store 仍不应成为所有状态的默认归宿:组件局部 UI 状态留在组件,服务端缓存交给请求层;持久化插件还要处理版本和敏感数据。

Q18: Vue 项目如何做性能优化?

答案

先用 Devtools 和浏览器工具定位更新、计算、DOM、网络或包体瓶颈。

  • 保持 props 稳定,减少不必要的深层响应式和深度 watch。
  • 长列表虚拟化,大组件拆分并按路由加载。
  • 静态内容使用编译器可优化的写法,避免模板中重复昂贵计算。

Q19: Vue SSR 和 hydration 有哪些注意点?

答案

服务端先生成 HTML,客户端用同一组件树接管交互。首屏标记必须确定一致,不能在渲染中直接依赖时间、随机数和仅浏览器存在的 API。

每个请求的状态要隔离,避免跨用户污染;数据获取、序列化和错误状态交给 SSR 框架统一管理更可靠。

  • SSR 首先在服务端生成 HTML,客户端用相同状态执行 hydration 并绑定事件。两端输出不一致会产生 hydration mismatch。
  • 时间、随机数、浏览器 API 和用户相关数据要延后到客户端或显式序列化;请求级状态必须隔离,避免跨用户污染。

Q20: Vue 2 升级 Vue 3 的主要风险是什么?

答案

不仅是改响应式 API,还涉及全局 API、生命周期、v-model、事件、插件、构建工具和第三方组件兼容。

先盘点依赖和废弃能力,建立测试与监控,按模块迁移并保留回滚。不要在同一次变更中同时大规模重构业务。

  • 主要风险包括 Proxy 响应式差异、生命周期名称变化、v-model/事件 API 调整、全局 API 改造以及第三方库兼容。
  • 实践中先升级到 Vue 2.7 并清理废弃写法,再使用兼容构建和迁移检查,按模块灰度替换,最后验证 SSR、路由和状态持久化。

Q21: Vue 2 的 Object.defineProperty 有哪些局限性?

答案

  1. 无法检测属性的添加和删除

    // Vue 2 中这些操作不会触发更新
    this.obj.newProp = 'value'; // ❌ 添加属性
    delete this.obj.prop; // ❌ 删除属性

    // 必须使用特殊 API
    this.$set(this.obj, 'newProp', 'value');
    this.$delete(this.obj, 'prop');
  2. 无法检测数组索引和长度变化

    this.arr[0] = 'new';    // ❌ 不会触发更新
    this.arr.length = 0; // ❌ 不会触发更新
  3. 初始化时需要递归遍历所有属性,性能开销大

  4. 需要重写数组方法(push、pop、splice 等)

Q22: 为什么解构 reactive 对象会丢失响应性?怎么处理?

答案

reactive 通过 Proxy 拦截对象属性访问。直接解构得到的是当时的普通值,后续读取不再经过原代理:

const state = reactive({ count: 0 });
const { count } = state; // 普通 number,不会继续更新

需要保留关联时,可以使用 toRef(state, 'count')toRefs(state),模板和组合式函数中再按 Ref 使用。对于只读计算值,可直接用 computed

不要无差别对大型对象调用 toRefs,它会为每个可枚举属性创建 Ref;更好的做法是只暴露组件真正需要的字段和操作。

Q23: Vue 2 和 Vue 3 的 Diff 算法有什么区别?

答案

特性Vue 2Vue 3
算法双端 Diff快速 Diff
核心优化头尾双指针最长递增子序列
静态提升
静态标记✅ PatchFlags

Vue 3 的快速 Diff 优势:

  1. 预处理相同前缀/后缀:先处理头尾不变的节点
  2. 最长递增子序列:最小化 DOM 移动操作
  3. 静态提升:静态节点跳过 Diff
  4. PatchFlags:精确标记动态内容类型

Q24: computed 可以有 setter 吗?适合什么场景?

答案

可以。传入 get/set 对象可创建可写 computed,常用于把组件的读写协议映射到已有状态,例如自定义 v-model

const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (value: string) => {
[firstName.value, lastName.value] = value.split(' ');
},
});

Getter 应保持纯粹并只做派生计算,Setter 负责把外部赋值转换成明确的状态更新。若写入涉及异步请求、重试或复杂副作用,更适合显式方法或 Action,不要隐藏在 computed setter 里。

Q25: 在哪个生命周期可以访问 DOM?

答案

mounted(或 onMounted 是第一个可以访问 DOM 的生命周期。

// ❌ beforeMount 中访问 DOM
onBeforeMount(() => {
console.log(document.querySelector('#app')); // null 或上一次的 DOM
});

// ✅ mounted 中访问 DOM
onMounted(() => {
console.log(document.querySelector('#app')); // ✅ 可以访问
const el = templateRef.value; // ✅ ref 可用
});

但要注意:mounted 不保证所有子组件都已挂载。如果需要等待整个视图渲染完成:

import { onMounted, nextTick } from 'vue';

onMounted(async () => {
await nextTick();
// 此时所有子组件都已渲染完成
});

Q26: 父组件如何调用子组件的方法?

答案

使用 ref + defineExpose

<!-- Parent.vue -->
<template>
<Child ref="childRef" />
</template>

<script setup lang="ts">
import { ref } from 'vue';
import Child from './Child.vue';

const childRef = ref<InstanceType<typeof Child>>();

// 调用子组件方法
childRef.value?.validate();
</script>
<!-- Child.vue -->
<script setup lang="ts">
function validate() {
// 验证逻辑
return true;
}

defineExpose({ validate });
</script>

Q27: 哪些组件不适合放进 keep-alive 缓存?

答案

keep-alive 适合切换后希望保留局部状态、DOM 和滚动位置的组件,但以下场景要谨慎:

  • 状态必须每次进入都重新初始化,例如支付或敏感表单。
  • 组件持有大量 DOM、图表实例或数据,缓存会造成明显内存压力。
  • 外部订阅在 deactivated 后仍持续运行,可能产生后台请求和泄漏。
  • 状态应由 URL 或 Store 恢复,而不是长期保留整个实例。

使用 include/exclude/max 控制范围,并在 activated/deactivated 中暂停和恢复资源。缓存命中要依赖稳定组件身份,动态路由还需明确按什么 key 区分实例。

Q28: v-oncev-memo 有什么区别?

答案

v-once 让子树在首次渲染后永久跳过后续更新,适合生命周期内确定不变的内容。

v-memo="[deps]" 根据依赖数组决定是否跳过该子树更新;依赖值都未变化时复用上次 VNode,适合经过测量后确认昂贵的大列表项或复杂子树。

二者都属于有明确前提的优化工具。依赖写错会产生陈旧 UI,简单组件使用它们可能比正常 Diff 更复杂。应先保证 props 稳定、减少无关响应式依赖,再用性能分析验证收益。

Q29: v-for 中的模板 ref 为什么通常得到数组?要注意什么?

答案

同一个模板 ref 出现在 v-for 中会对应多个元素或组件实例,因此挂载后通常得到集合。列表变化时,集合顺序不应被当作稳定业务映射。

更可靠的方式是使用函数 ref,按业务 ID 维护 Map:

const rows = new Map<string, HTMLElement>();

function bindRow(id: string, element: HTMLElement | null) {
if (element) rows.set(id, element);
else rows.delete(id);
}

DOM 只在挂载后可用,列表更新后读取要结合 nextTick。若只是为了定位、测量或滚动,也要评估是否能通过事件目标、CSS 或虚拟列表 API 避免持有大量节点引用。

Q30: 一个可复用 Composable 应怎样设计?

答案

Composable 应围绕一项明确能力组织响应式状态和生命周期,而不是把组件代码机械搬进函数。

设计重点:

  1. 输入同时考虑普通值、Ref 或 getter,必要时用 toValue 统一读取。
  2. 返回最小的只读状态和显式操作,避免调用方任意破坏内部不变量。
  3. 在所属 effect scope 内注册监听、定时器和订阅,并在清理时释放。
  4. SSR 时不要在模块顶层访问 window 或共享用户状态。
  5. 错误、loading、取消和竞态策略由能力本身统一定义。
  6. 类型参数只表达真实关系,不要为了“通用”堆叠难推断泛型。

Q31: defineModel 解决了什么问题?

答案

defineModel<script setup> 编译器宏,用来声明组件的 v-model 协议,减少手写 modelValue Prop 和 update:modelValue Emit 的样板代码。

<script setup lang="ts">
const value = defineModel<string>({ required: true });
</script>

<template>
<input v-model="value" />
</template>

命名模型、修饰符和 get/set 转换也可以配置。它只是生成受控数据协议,不代表子组件可以绕过父级随意修改其他 Prop;默认值还要小心父子初始状态不一致。

Q32: Teleport 和第三方 Portal 方案有什么区别?

答案

Teleport 是 Vue 3 内置能力,编译器、运行时、SSR 和组件生命周期都能理解它;Portal 是通用概念,Vue 2 常通过第三方库实现。

Teleport 只改变真实 DOM 的目标位置,逻辑父子关系、provide/inject、事件和 scoped 样式关系仍按 Vue 组件树工作。目标容器必须存在,SSR/Hydration 时还要保证服务端和客户端目标一致。

已有 Vue 3 项目通常优先使用 Teleport;只有需要额外的队列、层级管理或跨应用能力时,才在它之上封装浮层系统。

Q33: Hash 和 History 模式的区别?

答案

维度HashHistory
URL 美观/#/path(不美观)/path(美观)
服务器配置无需需要 fallback 配置
原理hashchangepopstate + pushState
兼容性IE8+IE10+
SEO# 后内容不被索引友好
请求服务器# 后内容不发送到服务器完整路径发送

Q34: 为什么 Pinia 移除了 mutations?

答案

Vuex 的 mutations 设计初衷是:

  1. 追踪状态变化:devtools 可以记录每次 mutation
  2. 保证同步:mutation 必须同步,方便调试

但实际开发中:

  1. 样板代码过多:每个状态变化都要写 mutation + action
  2. 命名冗余SET_USER, UPDATE_USER, CLEAR_USER...
  3. TypeScript 不友好:类型推断困难

Pinia 的解决方案:

  • 直接在 action 中修改状态:减少样板代码
  • devtools 仍然可以追踪:Pinia 同样支持时间旅行调试
  • 更好的 TS 支持:类型自动推断

Q35: Vue 的编译发生在什么时候?

答案

场景编译时机说明
SFC + 构建工具构建时vite/webpack 插件处理
运行时模板运行时需要完整版 Vue
JSX/TSX构建时Babel/esbuild 处理
// 构建时编译(推荐)
// .vue 文件在构建时被编译,产物只包含 render 函数

// 运行时编译(需要完整版 Vue)
import { createApp } from 'vue'; // 需要 'vue/dist/vue.esm-bundler.js'

createApp({
template: '<div>{{ msg }}</div>', // 运行时编译
data: () => ({ msg: 'Hello' })
});

Q36: Vue 应用首屏加载慢如何优化?

答案

优化方向具体措施
减少体积Tree Shaking、代码分割、压缩、Gzip
加快加载CDN、HTTP/2、预加载关键资源
优化渲染骨架屏、SSR/SSG、关键 CSS 内联
延迟加载路由懒加载、图片懒加载、异步组件
// 路由懒加载
const routes = [
{ path: '/', component: () => import('./Home.vue') }
];

// 组件懒加载
const HeavyComponent = defineAsyncComponent(() =>
import('./Heavy.vue')
);

// 第三方库 CDN
// vite.config.ts
export default {
build: {
rollupOptions: {
external: ['vue', 'vue-router'],
output: {
globals: {
vue: 'Vue',
'vue-router': 'VueRouter'
}
}
}
}
};

Q37: Vue 3 相比 Vue 2 有哪些优势?

答案

方面优势
响应式Proxy 替代 defineProperty,支持新增/删除属性、数组索引
性能编译优化(静态提升、PatchFlags)、更小体积、更快 SSR
APIComposition API 更好的逻辑复用和代码组织
TypeScript原生支持,类型推断更完善
新特性Fragment、Teleport、Suspense、多 v-model

Q38: Vue SSR 能带来什么收益,又增加哪些成本?

答案

SSR 在服务端先输出 HTML,可改善内容更早可见、链接分享和部分抓取体验,并能结合流式响应与服务端数据加载减少首屏请求瀑布。

它并不保证页面一定更快:服务端增加渲染和缓存压力,客户端仍可能承担 Hydration 与 JavaScript 执行;两端输出不一致还会导致 Hydration mismatch。

实际选型应按路由决定。内容和首屏敏感页面可使用 SSR/SSG,后台工具可能保留 CSR。生产中优先使用 Nuxt 等框架管理请求级状态、数据序列化、缓存、错误边界和部署运行时。

Q39: effectScope 解决什么问题?

答案

effectScope 可以把一组响应式副作用放进同一作用域统一停止,包括作用域中创建的 computedwatchwatchEffect。它适合组件之外的可复用逻辑、插件或临时业务会话,避免逐个保存停止函数。

在组件 setup 内创建的副作用通常已由组件生命周期管理,不需要为了形式再套一层。Composable 需要额外资源清理时,还应配合 onScopeDispose

停止作用域不会自动取消未接入清理逻辑的网络请求或定时器,这些资源仍要显式接入 Abort 或清理函数。

Q40: Vue 3 响应式系统如何处理嵌套对象?

答案

Vue 3 采用**懒代理(Lazy Proxy)**策略,只有在访问嵌套属性时才会递归创建代理:

const state = reactive({
user: {
profile: {
name: 'Tom'
}
}
});

// 初始化时只有最外层被代理
// 访问 state.user 时,user 对象被代理
// 访问 state.user.profile 时,profile 对象被代理
state.user.profile.name = 'Jerry'; // 触发更新

相比 Vue 2 的递归遍历,性能更好。

Q41: key 为什么不能用 index?

答案

使用 index 作为 key 会导致:

// 原始数据: [{id: 1, name: 'A'}, {id: 2, name: 'B'}]
// key 使用 index: [A:0, B:1]

// 删除第一项后: [{id: 2, name: 'B'}]
// key 变成: [B:0]

// Diff 认为:key=0 的节点内容从 A 变成 B
// 实际上应该:删除 A 节点,复用 B 节点

问题:

  1. 错误的节点复用:状态混乱
  2. 性能下降:本可以复用的节点被更新
  3. 表单输入丢失:输入框等组件状态错乱

Q42: watch 中怎样取消过期的异步副作用?

答案

监听值快速变化时,前一次请求可能晚于后一次返回,造成旧结果覆盖新结果。应在 watcher 清理阶段取消或忽略过期任务:

watch(query, async (value, _oldValue, onCleanup) => {
const controller = new AbortController();
onCleanup(() => controller.abort());

const response = await fetch(`/api/search?q=${value}`, {
signal: controller.signal,
});
result.value = await response.json();
});

清理函数会在下一次回调运行前或 watcher 停止时执行。还要区分取消错误和真实失败,并结合防抖、缓存与服务端限流,而不是只在 UI 层忽略所有响应。

Q43: createdmounted 有什么区别?数据请求应该放哪里?

答案

created 是 Options API 中实例已建立、响应式状态可用但 DOM 尚未挂载的阶段;mounted 发生在客户端 DOM 挂载后,适合测量元素或初始化依赖 DOM 的第三方库。

普通 CSR 中两处都能发请求,但“能发”不等于最佳位置:

  • 与 DOM 无关的数据不必等到 mounted。
  • SSR 中 mounted 不会在服务端执行;路由数据应优先交给框架的数据加载和缓存机制。
  • 请求要处理取消、竞态、错误和重复导航。
  • 组合式 API 中通常使用 setuponMounted 或框架提供的 useAsyncData 等能力。

Q44: provide/inject 和 props 有什么区别?

答案

维度propsprovide/inject
传递深度直接父子任意深度
数据流向单向向下向下(可伪双向)
类型推断自动需要手动指定
显式依赖否(隐式)
使用场景组件接口跨层级共享
// props:适合组件接口
<UserCard :user="user" />

// provide/inject:适合深层共享
// 主题、国际化、配置等全局性数据
provide('theme', theme);

Q45: activated 和 mounted 哪个先执行?

答案

  • 首次渲染mountedactivated
  • 从缓存恢复:只触发 activated(不会再触发 mounted
// 首次渲染
setup()onBeforeMount()onMounted()onActivated()

// 缓存后再次激活
onActivated() // 只触发这一个

// 进入缓存
onDeactivated()

Q46: v-if 和 v-for 能一起用吗?

答案

Vue 3 中不推荐一起使用v-if 优先级高于 v-for

<!-- ❌ 错误:v-if 无法访问 v-for 的 item -->
<li v-for="item in list" v-if="item.active">
{{ item.name }}
</li>

<!-- ✅ 正确方案1:使用 computed 过滤 -->
<li v-for="item in activeItems">
{{ item.name }}
</li>

<script setup lang="ts">
const activeItems = computed(() =>
list.value.filter(item => item.active)
);
</script>

<!-- ✅ 正确方案2:外层包装 template -->
<template v-for="item in list" :key="item.id">
<li v-if="item.active">{{ item.name }}</li>
</template>

Q47: 为什么不能用随机数作为 key?

答案

每次渲染都生成新的随机数,导致:

<!-- ❌ 每次渲染 key 都不同 -->
<li v-for="item in list" :key="Math.random()">
{{ item.name }}
</li>

问题:

  1. 无法复用:每次 key 都是新的,所有节点都被认为是新增
  2. 性能下降:全部销毁重建,失去 Diff 优化
  3. 状态丢失:组件状态每次都重置

Q48: Options API 还有存在的必要吗?

答案

。两种 API 会长期共存:

  1. 学习曲线更低:对初学者更友好
  2. 简单场景够用:小型组件用 Options API 更直观
  3. 迁移成本:大量 Vue 2 项目仍在使用
  4. 混用支持:Vue 3 允许在同一组件中混用
// Vue 3 中混用两种 API
export default {
data() {
return { count: 0 };
},
setup() {
const name = ref('Vue');
return { name };
}
};

Q49: defineProps 是什么?为什么不需要 import?

答案

defineProps编译器宏(Compiler Macro),不是运行时函数:

// 编译前
const props = defineProps<{ msg: string }>();

// 编译后
const props = __props; // 直接引用组件的 props

特点:

  1. 无需导入:编译时被处理,不存在于运行时
  2. 类型安全:支持 TypeScript 泛型语法
  3. 编译优化:生成更高效的代码

类似的编译器宏还有:defineEmitsdefineExposedefineOptionsdefineSlotsdefineModelwithDefaults

Q50: Teleport 的内容属于哪个组件?

答案

逻辑上仍属于原组件,只是 DOM 渲染位置改变:

<template>
<Teleport to="body">
<!-- 这里的 props 和 events 仍然属于当前组件 -->
<Modal :title="title" @close="handleClose" />
</Teleport>
</template>

<script setup>
// title 和 handleClose 来自当前组件
const title = ref('Modal Title');
function handleClose() { /* ... */ }
</script>
  • props/emits:正常工作
  • provide/inject:可以跨 Teleport 传递
  • 生命周期:与父组件关联
  • 样式作用域:scoped CSS 仍然生效

Q51: 路由懒加载是如何实现的?

答案

// 路由懒加载利用动态 import + Webpack/Vite 代码分割

// 编译前
const routes = [
{ path: '/about', component: () => import('./About.vue') }
];

// Webpack 编译后,About.vue 被打包成单独的 chunk
// 访问 /about 时才下载 about.chunk.js

// Vite 编译后生成
// /assets/About.abc123.js

// 原理:
// 1. () => import() 返回 Promise
// 2. 路由激活时,Vue Router 调用该函数
// 3. 浏览器动态加载对应的 chunk
// 4. 加载完成后渲染组件

配合 Suspense 显示加载状态:

<template>
<RouterView v-slot="{ Component }">
<Suspense>
<component :is="Component" />
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</RouterView>
</template>

Q52: Pinia 如何实现响应式?

答案

Pinia 基于 Vue 3 的响应式系统:

// defineStore 内部实现(简化)
function defineStore(id, setup) {
return function useStore() {
// 检查是否已创建
if (!pinia._s.has(id)) {
// 创建响应式 store
const store = reactive({});

// 执行 setup 函数
const setupResult = setup();

// 合并到 store
Object.assign(store, setupResult);

pinia._s.set(id, store);
}

return pinia._s.get(id);
};
}

核心是使用 reactive 包装 store 对象。

Q53: Vue 3 编译优化有哪些?

答案

优化说明效果
PatchFlags标记动态内容类型Diff 时精确更新
静态提升静态节点只创建一次减少内存分配
事件缓存缓存事件处理函数避免子组件重渲染
Block Tree收集动态节点Diff 跳过静态节点
预字符串化连续静态节点合并为字符串减少 VNode 数量

Q54: Vue 长列表应该怎样优化?

答案

先判断瓶颈是一次性 DOM 数量、更新频率、数据计算还是网络加载,再选择方案:

  1. 数据量极大时使用虚拟列表,只渲染可见区域,并处理动态高度、滚动锚定和可访问性。
  2. 分页或无限加载减少一次获取的数据,配合取消、缓存和去重。
  3. 保持 item 的 key 与 Props 稳定,拆分高频更新区域。
  4. 大型不可变数据可使用 shallowRef,第三方实例可用 markRaw,避免无意义深层代理。
  5. 经测量后再考虑 v-memo,不要把 Object.freeze 当作 Vue 3 的通用响应式优化开关。

最终用 Vue Devtools 和浏览器 Performance 验证渲染数量、脚本时间和滚动帧率。

Q55: Vue 3 为什么不支持 IE?旧系统应该怎么办?

答案

Vue 3 的响应式核心依赖 Proxy,而 IE 不支持 Proxy,也无法用 JavaScript 完整 polyfill。Vue 3 还基于现代模块、语法和浏览器平台能力设计,因此官方没有提供 IE 兼容构建。

旧系统的处理原则是:

  • 先确认是否真的存在活跃 IE 用户和合规要求,不为假设兼容长期冻结技术栈。
  • 必须支持时,将旧应用隔离维护并控制安全风险,不再把已经结束维护的 Vue 2 当作长期安全方案。
  • 通过服务端渲染的只读降级页、独立旧版入口或业务下线计划逐步迁移。
  • 新功能与现代浏览器版本分离,避免把整个主应用拖回旧工具链。

Q56: Vue SSR 中为什么每个请求都要创建新的应用实例?

答案

Node.js 服务端是一个长期运行的进程。如果所有请求共用同一个 Vue 实例和 Pinia store,用户 A 修改的状态会被用户 B 看到,造成跨请求状态污染。因此必须在每个请求中调用 createSSRApp()createPinia() 创建全新实例。

Q57: 什么是惰性代理?它有什么好处?

答案

惰性代理(Lazy Proxy)指的是只有在访问嵌套对象时,才对其进行代理,而不是在初始化时递归代理所有属性。

// Vue 2:初始化时递归所有属性(即使从不访问)
const data = { a: { b: { c: 1 } } };
// 会立即递归处理 a、a.b、a.b.c

// Vue 3:只在访问时才代理
const proxy = reactive({ a: { b: { c: 1 } } });
// 只有访问 proxy.a 时才代理 a 对象
// 只有访问 proxy.a.b 时才代理 b 对象

好处

  1. 初始化更快:不需要遍历所有属性
  2. 内存占用更低:未访问的属性不会被代理
  3. 按需代理:对大型数据结构尤其有效

Q58: shallowRef 和 shallowReactive 的作用是什么?

答案

它们只对第一层数据做响应式处理,常用于性能优化

import { shallowRef, shallowReactive, triggerRef } from 'vue';

// shallowRef:只有 .value 的替换是响应式的
const state = shallowRef({ count: 0 });
state.value.count++; // ❌ 不会触发更新
state.value = { count: 1 }; // ✅ 触发更新

// 手动触发更新
state.value.count++;
triggerRef(state);

// shallowReactive:只有第一层属性是响应式的
const shallow = shallowReactive({
nested: { count: 0 }
});
shallow.nested = { count: 1 }; // ✅ 触发更新
shallow.nested.count++; // ❌ 不会触发更新

使用场景:大型不可变数据结构、第三方库对象。

Q59: 虚拟 DOM 一定比直接操作 DOM 快吗?

答案

不一定。虚拟 DOM 还需要创建 VNode、执行协调并最终操作真实 DOM;对一个已知节点做一次精准修改,原生 DOM 通常更直接。

它的主要价值是让开发者用声明式状态描述 UI,并由框架统一组织更新、批处理、组件组合和跨平台渲染。Vue 还通过模板编译生成 PatchFlag、静态提升等信息,减少运行时需要比较的内容。

性能应在完整应用的数据流和维护成本下评估。极高频绘图或十万级图元可能更适合 Canvas/WebGL,但普通业务界面不应为了理论上的“少一层”放弃框架状态模型。

Q60: computed 的缓存是如何实现的?

答案

computed 使用 dirty 标志位实现缓存:

function computed(getter) {
let cachedValue;
let dirty = true; // 脏标志

const effect = new ReactiveEffect(getter, () => {
// 依赖变化时,标记为脏,但不立即计算
dirty = true;
});

return {
get value() {
if (dirty) {
// 脏时才重新计算
cachedValue = effect.run();
dirty = false;
}
return cachedValue;
}
};
}
  1. 首次访问dirty=true,执行 getter,缓存结果,dirty=false
  2. 再次访问dirty=false,直接返回缓存
  3. 依赖变化dirty=true,下次访问时重新计算

相关链接