Vue 面试题库
一、Vue 基础
1.1 什么是 Vue.js?它的核心特性是什么?
面试回答思路:
Vue.js 是一个渐进式 JavaScript 框架,主要用于构建用户界面。我可以从以下几个方面来介绍它的核心特性:
1. 响应式数据绑定
- Vue 会自动追踪数据的依赖关系,当数据发生变化时,视图会自动更新
- 开发者不需要手动操作 DOM,只需要关注数据的变化
- 这大大简化了开发流程,提高了开发效率
2. 组件化开发
- Vue 采用组件化的开发模式,每个组件都是独立的、可复用的
- 组件之间通过 props 和事件进行通信
- 这种模式让代码更易维护,也便于团队协作
3. 虚拟 DOM
- Vue 使用虚拟 DOM 来提高渲染性能
- 通过 diff 算法,只更新真正需要变化的部分
- 减少了直接操作 DOM 的次数,提升了应用性能
4. 指令系统
- Vue 提供了丰富的指令,如 v-if、v-for、v-model 等
- 这些指令让模板更加简洁和易读
- 也可以自定义指令来扩展功能
5. 渐进式框架
- Vue 的核心库只关注视图层,可以逐步集成到现有项目中
- 不需要一次性重写整个应用
- 可以根据项目需求选择性地使用 Vue Router、Vuex 等配套工具
补充说明: 在实际项目中,我使用 Vue 开发过多个项目,它的学习曲线相对平缓,文档也很完善,对新手比较友好。同时 Vue 3 引入了 Composition API,让代码组织更加灵活,TypeScript 支持也更好了。
1// Vue 3 基本示例2import { createApp, ref } from 'vue'34const app = createApp({5 setup() {6 const message = ref('Hello Vue!')7 8 const updateMessage = () => {9 message.value = 'Hello World!'10 }11 12 return {13 message,14 updateMessage15 }16 }17})1819app.mount('#app')1.2 Vue 2 和 Vue 3 的主要区别是什么?
面试回答思路:
Vue 3 相比 Vue 2 有很多重要的改进,我主要从以下几个方面来说明:
1. 性能提升
- Vue 3 的渲染速度比 Vue 2 快了约 1.3-2 倍
- 这主要得益于重写了虚拟 DOM 的实现
- 内存占用也减少了约 54%
- 打包体积更小,支持 Tree-shaking,可以按需引入
2. Composition API
- Vue 3 引入了 Composition API,这是一种新的代码组织方式
- 相比 Options API,它能更好地组织和复用逻辑
- 特别适合大型项目和复杂组件
- 但 Options API 仍然可以使用,向后兼容
3. 响应式系统升级
- Vue 2 使用 Object.defineProperty 实现响应式
- Vue 3 改用 Proxy,性能更好
- 可以监听动态添加的属性,不需要 Vue.set
- 可以监听数组索引和 length 的变化
4. TypeScript 支持
- Vue 3 使用 TypeScript 重写,类型推导更准确
- 开发体验大幅提升
- IDE 的代码提示和类型检查更完善
5. 新特性
- 支持多个根节点(Fragment)
- Teleport 组件,可以将内容渲染到 DOM 的其他位置
- Suspense 组件,更好地处理异步组件
- 更好的自定义渲染器 API
实际应用建议: 如果是新项目,我会推荐使用 Vue 3,因为它性能更好,功能也更强大。但如果是维护老项目,Vue 2 也完全够用,而且生态更成熟。Vue 3 的迁移成本需要评估,不是所有项目都需要立即升级。
主要区别:
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| API 风格 | Options API | Composition API(可选) |
| 响应式系统 | Object.defineProperty | Proxy |
| 性能 | 较好 | 更快(约 1.3-2 倍) |
| TypeScript 支持 | 有限 | 完整支持 |
| 包体积 | 较大 | 更小(Tree-shaking) |
| 生命周期 | beforeCreate, created 等 | setup, onMounted 等 |
| 多根节点 | 不支持 | 支持(Fragment) |
| Teleport | 不支持 | 支持 |
| Suspense | 不支持 | 支持 |
- Vue 2
- Vue 3
1export default {2 data() {3 return {4 count: 05 }6 },7 methods: {8 increment() {9 this.count++10 }11 },12 mounted() {13 console.log('Component mounted')14 }15}1import { ref, onMounted } from 'vue'23export default {4 setup() {5 const count = ref(0)6 7 const increment = () => {8 count.value++9 }10 11 onMounted(() => {12 console.log('Component mounted')13 })14 15 return {16 count,17 increment18 }19 }20}1.3 解释 Vue 的响应式原理
面试回答思路:
Vue 的响应式原理是面试中的高频考点,我会分别介绍 Vue 2 和 Vue 3 的实现方式:
Vue 2 的响应式原理(基于 Object.defineProperty):
当我们创建一个 Vue 实例时,Vue 会遍历 data 对象的所有属性,使用 Object.defineProperty 将这些属性转换为 getter 和 setter。
工作流程:
- 初始化阶段:Vue 会递归遍历 data 对象,为每个属性添加 getter 和 setter
- 依赖收集:当组件渲染时,会访问数据的 getter,此时会收集当前组件作为依赖(Watcher)
- 派发更新:当数据被修改时,会触发 setter,setter 会通知所有依赖的 Watcher 进行更新
- 异步更新队列:Vue 会将所有的更新放入一个队列中,在下一个事件循环中统一执行,避免重复渲染
Vue 2 的局限性:
- 无法检测对象属性的添加或删除,需要使用 Vue.set
- 无法检测数组索引和 length 的变化
- 需要递归遍历所有属性,初始化性能开销大
Vue 3 的响应式原理(基于 Proxy):
Vue 3 使用 ES6 的 Proxy 来实现响应式,这是一个更强大的方案。
Proxy 的优势:
- 可以监听整个对象:不需要遍历每个属性
- 可以监听动态添加的属性:新增属性自动具有响应式
- 可以监听数组的变化:包括索引和 length
- 性能更好:懒代理,只有访问到的属性才会被代理
- 支持 Map、Set 等数据结构
实现细节:
- 使用 Proxy 的 get 拦截器进行依赖收集(track)
- 使用 Proxy 的 set 拦截器触发更新(trigger)
- 使用 Reflect 确保正确的 this 指向
总结: Vue 的响应式系统是其核心特性,理解它的原理对于调试问题和性能优化都很有帮助。Vue 3 的 Proxy 方案解决了 Vue 2 的很多痛点,是一个重要的升级。
1// Vue 2 响应式简化实现2function defineReactive(obj, key, val) {3 const dep = new Dep() // 依赖收集器4 5 Object.defineProperty(obj, key, {6 get() {7 if (Dep.target) {8 dep.depend() // 收集依赖9 }10 return val11 },12 set(newVal) {13 if (newVal !== val) {14 val = newVal15 dep.notify() // 通知更新16 }17 }18 })19}Vue 3 响应式原理(Proxy):
- 使用 Proxy 代理整个对象
- 可以监听动态添加的属性
- 可以监听数组索引和 length 变化
- 性能更好
1// Vue 3 响应式简化实现2function reactive(target) {3 return new Proxy(target, {4 get(target, key, receiver) {5 track(target, key) // 收集依赖6 return Reflect.get(target, key, receiver)7 },8 set(target, key, value, receiver) {9 const result = Reflect.set(target, key, value, receiver)10 trigger(target, key) // 触发更新11 return result12 }13 })14}二、组件通信
2.1 Vue 组件间通信的方式有哪些?
面试回答思路:
Vue 组件间通信是开发中经常遇到的问题,根据组件关系的不同,有多种通信方式可以选择:
1. Props 和 $emit(父子组件通信)
这是最基本也是最常用的通信方式:
- Props:父组件向子组件传递数据,是单向数据流
- $emit:子组件通过事件向父组件传递数据
- 优点:简单直观,符合单向数据流的设计理念
- 适用场景:父子组件之间的直接通信
2. Provide 和 Inject(跨层级通信)
用于祖先组件向后代组件传递数据:
- 祖先组件通过 provide 提供数据
- 后代组件通过 inject 注入数据
- 优点:可以跨越多层组件传递数据,不需要逐层传递
- 缺点:数据来源不够明确,不是响应式的(Vue 2)
- 适用场景:主题、语言等全局配置
3. EventBus(兄弟组件通信 - Vue 2)
通过一个空的 Vue 实例作为事件总线:
- 任何组件都可以触发和监听事件
- 优点:简单灵活
- 缺点:事件管理混乱,难以维护,Vue 3 已移除
- 适用场景:小型项目的兄弟组件通信
4. Vuex 或 Pinia(全局状态管理)
用于管理应用的全局状态:
- 集中式存储和管理应用的所有组件状态
- 优点:状态可预测,易于调试,支持时间旅行
- 缺点:增加了代码复杂度
- 适用场景:中大型项目,多个组件共享状态
5. $refs(父组件访问子组件)
父组件可以直接访问子组件的实例:
- 可以调用子组件的方法或访问数据
- 优点:直接方便
- 缺点:破坏了组件的封装性,不推荐过度使用
- 适用场景:需要直接操作子组件的特殊情况
6. $parent 和 $children(不推荐)
可以访问父组件或子组件实例:
- 耦合度太高,不利于维护
- Vue 3 已移除 $children
- 不推荐使用
选择建议:
- 父子组件:优先使用 Props/$emit
- 跨层级:使用 Provide/Inject
- 全局状态:使用 Pinia(Vue 3)或 Vuex
- 避免使用 EventBus 和 $parent/$children
在实际项目中,我会根据组件关系和数据流向选择合适的通信方式,保持代码的清晰和可维护性。
1. Props / $emit(父子组件)
1<!-- 父组件 -->2<template>3 <ChildComponent :message="parentMsg" @update="handleUpdate" />4</template>56<script>7export default {8 data() {9 return {10 parentMsg: 'Hello from parent'11 }12 },13 methods: {14 handleUpdate(data) {15 console.log('Received from child:', data)16 }17 }18}19</script>2021<!-- 子组件 -->22<template>23 <div>24 <p>{{ message }}</p>25 <button @click="sendToParent">Send to Parent</button>26 </div>27</template>2829<script>30export default {31 props: ['message'],32 methods: {33 sendToParent() {34 this.$emit('update', 'Data from child')35 }36 }37}38</script>2. Provide / Inject(跨层级)
1// 祖先组件2export default {3 provide() {4 return {5 theme: 'dark',6 user: this.currentUser7 }8 }9}1011// 后代组件12export default {13 inject: ['theme', 'user'],14 mounted() {15 console.log(this.theme) // 'dark'16 }17}3. EventBus(兄弟组件 - Vue 2)
1// eventBus.js2import Vue from 'vue'3export const EventBus = new Vue()45// 组件 A6EventBus.$emit('custom-event', data)78// 组件 B9EventBus.$on('custom-event', (data) => {10 console.log(data)11})4. Vuex / Pinia(全局状态管理)
1// Pinia Store2import { defineStore } from 'pinia'34export const useUserStore = defineStore('user', {5 state: () => ({6 name: 'John',7 age: 308 }),9 actions: {10 updateName(newName) {11 this.name = newName12 }13 }14})1516// 组件中使用17import { useUserStore } from '@/stores/user'1819const userStore = useUserStore()20console.log(userStore.name)21userStore.updateName('Jane')5. $refs(父访问子)
1<template>2 <ChildComponent ref="childRef" />3 <button @click="callChildMethod">Call Child</button>4</template>56<script>7export default {8 methods: {9 callChildMethod() {10 this.$refs.childRef.someMethod()11 }12 }13}14</script>6. $parent / $children(不推荐)
1// 子组件访问父组件2this.$parent.someMethod()34// 父组件访问子组件5this.$children[0].someMethod()2.2 v-model 的原理是什么?
面试回答思路:
v-model 是 Vue 中实现双向数据绑定的语法糖,理解它的原理对于自定义组件很有帮助。
本质原理:
v-model 实际上是两个操作的组合:
- 数据绑定:将数据绑定到表单元素的 value 属性
- 事件监听:监听表单元素的 input 事件,更新数据
简单来说,v-model="message" 等价于 :value="message" 加上 @input="message = $event.target.value"
在原生表单元素上:
- 对于 input、textarea:监听 input 事件,绑定 value 属性
- 对于 checkbox、radio:监听 change 事件,绑定 checked 属性
- 对于 select:监听 change 事件,绑定 value 属性
在自定义组件上:
Vue 2 和 Vue 3 的实现略有不同:
Vue 2:
- 默认使用 value 作为 prop
- 默认监听 input 事件
- 可以通过 model 选项自定义 prop 和事件名
Vue 3:
- 默认使用 modelValue 作为 prop
- 默认监听 update:modelValue 事件
- 支持多个 v-model,如 v-model:name、v-model:email
修饰符:
v-model 还支持几个常用的修饰符:
.lazy:将 input 事件改为 change 事件,失去焦点时才更新.number:自动将输入值转换为数字类型.trim:自动去除首尾空格
实际应用:
在开发自定义表单组件时,我经常需要实现 v-model 支持。理解它的原理后,就能轻松实现组件的双向绑定,让组件的使用更加简洁。Vue 3 的多 v-model 特性让复杂表单组件的开发更加灵活。
- Vue 2
- Vue 3
1<!-- 使用 v-model -->2<input v-model="message" />34<!-- 等价于 -->5<input 6 :value="message" 7 @input="message = $event.target.value"8/>910<!-- 自定义组件 -->11<CustomInput v-model="value" />1213<!-- 等价于 -->14<CustomInput 15 :value="value" 16 @input="value = $event"17/>1819<!-- CustomInput 组件实现 -->20<template>21 <input 22 :value="value" 23 @input="$emit('input', $event.target.value)"24 />25</template>2627<script>28export default {29 props: ['value']30}31</script>1<!-- 使用 v-model -->2<input v-model="message" />34<!-- 等价于 -->5<input 6 :modelValue="message" 7 @update:modelValue="message = $event"8/>910<!-- 自定义组件 -->11<CustomInput v-model="value" />1213<!-- 等价于 -->14<CustomInput 15 :modelValue="value" 16 @update:modelValue="value = $event"17/>1819<!-- CustomInput 组件实现 -->20<template>21 <input 22 :value="modelValue" 23 @input="$emit('update:modelValue', $event.target.value)"24 />25</template>2627<script>28export default {29 props: ['modelValue'],30 emits: ['update:modelValue']31}32</script>3334<!-- Vue 3 支持多个 v-model -->35<UserForm 36 v-model:name="userName" 37 v-model:email="userEmail"38/>三、生命周期
3.1 Vue 的生命周期钩子有哪些?
面试回答思路:
Vue 的生命周期是指组件从创建到销毁的整个过程,Vue 在不同阶段提供了钩子函数,让我们可以在特定时机执行代码。
Vue 2 的生命周期钩子:
我按照执行顺序来介绍:
1. 创建阶段:
- beforeCreate:实例初始化之后,数据观测和事件配置之前调用。此时无法访问 data、computed、methods 等。很少使用。
- created:实例创建完成后调用。此时可以访问 data、computed、methods,但 DOM 还未生成。常用于数据初始化、API 调用。
2. 挂载阶段:
- beforeMount:在挂载开始之前调用,相关的 render 函数首次被调用。很少使用。
- mounted:实例被挂载后调用,此时可以访问 DOM。常用于 DOM 操作、第三方库初始化、图表渲染等。
3. 更新阶段:
- beforeUpdate:数据更新时调用,发生在虚拟 DOM 重新渲染之前。可以访问更新前的 DOM。
- updated:数据更新导致的虚拟 DOM 重新渲染后调用。可以访问更新后的 DOM,但要避免在这里修改数据,可能导致无限循环。
4. 销毁阶段:
- beforeDestroy:实例销毁之前调用。常用于清理定时器、取消事件监听、取消网络请求等。
- destroyed:实例销毁后调用。此时所有的事件监听器已被移除,子实例也被销毁。
5. 特殊钩子(keep-alive):
- activated:被 keep-alive 缓存的组件激活时调用
- deactivated:被 keep-alive 缓存的组件停用时调用
Vue 3 的变化:
Vue 3 在 Composition API 中对生命周期钩子进行了重命名:
- beforeCreate 和 created 被 setup() 替代
- beforeDestroy 改名为 beforeUnmount
- destroyed 改名为 unmounted
- 其他钩子在前面加上 "on",如 onMounted、onUpdated
使用建议:
- 数据初始化、API 调用:放在 created 或 setup 中
- DOM 操作:放在 mounted 中
- 清理工作:放在 beforeUnmount 中
- 避免在 updated 中修改数据:容易造成死循环
实际经验:
在项目中,我最常用的是 created/setup(数据初始化)、mounted(DOM 操作)和 beforeUnmount(清理)。理解生命周期对于调试组件问题、优化性能都很重要。
Vue 2 生命周期:
| 钩子 | 说明 | 使用场景 |
|---|---|---|
| beforeCreate | 实例初始化后,数据观测前 | 很少使用 |
| created | 实例创建完成,数据观测完成 | 数据初始化、API 调用 |
| beforeMount | 挂载开始前 | 很少使用 |
| mounted | 挂载完成,DOM 可访问 | DOM 操作、第三方库初始化 |
| beforeUpdate | 数据更新前 | 访问更新前的 DOM |
| updated | 数据更新后 | 访问更新后的 DOM |
| beforeDestroy | 实例销毁前 | 清理定时器、事件监听 |
| destroyed | 实例销毁后 | 清理工作 |
| activated | keep-alive 组件激活 | 缓存组件恢复 |
| deactivated | keep-alive 组件停用 | 缓存组件暂停 |
Vue 3 生命周期:
| Vue 2 | Vue 3 (Composition API) | 说明 |
|---|---|---|
| beforeCreate | setup() | 组件创建前 |
| created | setup() | 组件创建后 |
| beforeMount | onBeforeMount | 挂载前 |
| mounted | onMounted | 挂载后 |
| beforeUpdate | onBeforeUpdate | 更新前 |
| updated | onUpdated | 更新后 |
| beforeUnmount | onBeforeUnmount | 卸载前 |
| unmounted | onUnmounted | 卸载后 |
| activated | onActivated | 激活 |
| deactivated | onDeactivated | 停用 |
1// Vue 3 Composition API 示例2import { ref, onMounted, onUpdated, onUnmounted } from 'vue'34export default {5 setup() {6 const count = ref(0)7 8 onMounted(() => {9 console.log('Component mounted')10 // DOM 操作、API 调用11 })12 13 onUpdated(() => {14 console.log('Component updated')15 })16 17 onUnmounted(() => {18 console.log('Component unmounted')19 // 清理工作20 })21 22 return { count }23 }24}3.2 父子组件生命周期执行顺序
面试回答思路:
父子组件的生命周期执行顺序是一个经典的面试题,理解这个顺序对于调试组件问题很有帮助。
核心原则:
Vue 的组件渲染是一个递归的过程,遵循"深度优先"的原则。简单来说就是:
- 创建过程:从外到内(父 → 子)
- 挂载过程:从内到外(子 → 父)
详细执行顺序:
1. 挂载阶段(初始化):
1父 beforeCreate 2→ 父 created 3→ 父 beforeMount 4→ 子 beforeCreate 5→ 子 created 6→ 子 beforeMount 7→ 子 mounted 8→ 父 mounted为什么是这个顺序?
- 父组件先开始创建和初始化
- 当父组件开始挂载时,遇到子组件标签
- 子组件开始创建、初始化、挂载
- 子组件挂载完成后,父组件才算挂载完成
2. 更新阶段:
1父 beforeUpdate 2→ 子 beforeUpdate 3→ 子 updated 4→ 父 updated说明:
- 父组件的数据变化可能影响子组件
- 子组件先完成更新
- 父组件最后完成更新
3. 销毁阶段:
1父 beforeDestroy 2→ 子 beforeDestroy 3→ 子 destroyed 4→ 父 destroyed说明:
- 父组件先开始销毁
- 子组件完全销毁后
- 父组件才完成销毁
实际应用场景:
- 数据初始化时机:如果子组件依赖父组件的数据,要确保在父组件 created 之后传递
- DOM 操作时机:如果需要操作子组件的 DOM,要在父组件 mounted 之后
- 清理资源:父组件销毁时,子组件会自动销毁,不需要手动处理
记忆技巧:
可以把组件想象成俄罗斯套娃:
- 组装时:先组装外层,再组装内层,最后内层装好了,外层才算装好
- 拆卸时:先拆外层,再拆内层,最后内层拆完了,外层才算拆完
这个顺序在调试问题时特别有用,比如子组件的 mounted 钩子中可以确保父组件的 DOM 已经存在。
四、指令与过滤器
4.1 常用的 Vue 指令有哪些?
面试回答思路:
Vue 的指令系统是其模板语法的核心,我会介绍最常用的几个指令及其使用场景:
1. v-if / v-else-if / v-else(条件渲染)
这是真正的条件渲染,元素会根据条件被创建或销毁:
- 适用于条件不经常改变的场景
- 有更高的切换开销
- 可以配合 template 标签使用,不会渲染额外的 DOM 元素
- 惰性渲染:初始条件为 false 时,不会渲染
2. v-show(条件显示)
通过 CSS 的 display 属性控制显示隐藏:
- 适用于频繁切换的场景
- 有更高的初始渲染开销
- 元素始终会被渲染,只是切换 display 属性
- 不支持 template 标签
选择建议:
- 频繁切换用 v-show
- 运行时条件很少改变用 v-if
- 涉及权限控制用 v-if(不渲染 DOM 更安全)
3. v-for(列表渲染)
用于渲染列表数据:
- 必须使用 key 属性,且 key 要唯一
- 可以遍历数组、对象、数字
- 可以配合 template 使用
- 不要和 v-if 一起使用(优先级问题)
4. v-bind(属性绑定,简写 :)
动态绑定 HTML 属性:
- 可以绑定任何 HTML 属性
- 支持对象和数组语法(class 和 style)
- 可以绑定 props
5. v-on(事件监听,简写 @)
绑定事件监听器:
- 支持所有原生事件
- 支持事件修饰符:.stop、.prevent、.capture、.self、.once、.passive
- 支持按键修饰符:.enter、.tab、.delete、.esc、.space 等
- 可以传递参数和 $event 对象
6. v-model(双向绑定)
表单输入和应用状态的双向绑定:
- 支持修饰符:.lazy、.number、.trim
- 本质是 :value 和 @input 的语法糖
- 可用于自定义组件
7. v-slot(插槽,简写 #)
用于分发内容:
- 默认插槽、具名插槽、作用域插槽
- 让组件更加灵活和可复用
8. v-pre(跳过编译)
跳过这个元素和它的子元素的编译过程:
- 可以用来显示原始 Mustache 标签
- 加快编译速度
9. v-once(只渲染一次)
只渲染元素和组件一次,后续不再更新:
- 用于优化性能
- 适用于静态内容
10. v-cloak(隐藏未编译的模板)
保持在元素上直到关联实例结束编译:
- 配合 CSS 规则使用
- 解决页面闪烁问题
实际应用经验:
在项目中,我最常用的是 v-if、v-for、v-bind、v-on 和 v-model。理解每个指令的特点和适用场景,可以写出更高效、更易维护的代码。特别是 v-if 和 v-show 的选择,以及 v-for 必须使用 key,这些都是实际开发中的重点。
1. v-if / v-else-if / v-else:条件渲染
1<template>2 <div v-if="type === 'A'">A</div>3 <div v-else-if="type === 'B'">B</div>4 <div v-else>C</div>5</template>2. v-show:条件显示(CSS display)
1<div v-show="isVisible">Content</div>3. v-for:列表渲染
1<ul>2 <li v-for="(item, index) in items" :key="item.id">3 {{ index }}: {{ item.name }}4 </li>5</ul>4. v-bind(:):属性绑定
1<img :src="imageSrc" :alt="imageAlt" />2<div :class="{ active: isActive }" :style="{ color: textColor }"></div>5. v-on(@):事件监听
1<button @click="handleClick">Click</button>2<input @input="handleInput" @keyup.enter="handleEnter" />6. v-model:双向绑定
1<input v-model="message" />2<input v-model.trim="username" />3<input v-model.number="age" />7. v-slot(#):插槽
1<template #header>2 <h1>Header Content</h1>3</template>8. v-pre:跳过编译
1<span v-pre>{{ this will not be compiled }}</span>9. v-once:只渲染一次
1<span v-once>{{ message }}</span>10. v-cloak:隐藏未编译的模板
1<style>2[v-cloak] { display: none; }3</style>45<div v-cloak>{{ message }}</div>4.2 如何自定义指令?
答案:
- Vue 2
- Vue 3
1// 全局注册2Vue.directive('focus', {3 inserted(el) {4 el.focus()5 }6})78// 局部注册9export default {10 directives: {11 focus: {12 inserted(el) {13 el.focus()14 }15 }16 }17}1819// 完整钩子函数20Vue.directive('example', {21 bind(el, binding, vnode) {22 // 只调用一次,指令第一次绑定到元素时23 },24 inserted(el, binding, vnode) {25 // 被绑定元素插入父节点时26 },27 update(el, binding, vnode, oldVnode) {28 // 所在组件的 VNode 更新时29 },30 componentUpdated(el, binding, vnode, oldVnode) {31 // 所在组件的 VNode 及其子 VNode 全部更新后32 },33 unbind(el, binding, vnode) {34 // 只调用一次,指令与元素解绑时35 }36})1// 全局注册2app.directive('focus', {3 mounted(el) {4 el.focus()5 }6})78// 局部注册9export default {10 directives: {11 focus: {12 mounted(el) {13 el.focus()14 }15 }16 }17}1819// 完整钩子函数20app.directive('example', {21 created(el, binding, vnode, prevVnode) {},22 beforeMount(el, binding, vnode, prevVnode) {},23 mounted(el, binding, vnode, prevVnode) {},24 beforeUpdate(el, binding, vnode, prevVnode) {},25 updated(el, binding, vnode, prevVnode) {},26 beforeUnmount(el, binding, vnode, prevVnode) {},27 unmounted(el, binding, vnode, prevVnode) {}28})实用自定义指令示例:
1// 1. 权限指令2app.directive('permission', {3 mounted(el, binding) {4 const { value } = binding5 const permissions = store.state.user.permissions6 7 if (!permissions.includes(value)) {8 el.parentNode?.removeChild(el)9 }10 }11})1213// 使用14<button v-permission="'admin'">Delete</button>1516// 2. 防抖指令17app.directive('debounce', {18 mounted(el, binding) {19 let timer20 el.addEventListener('click', () => {21 if (timer) clearTimeout(timer)22 timer = setTimeout(() => {23 binding.value()24 }, 500)25 })26 }27})2829// 使用30<button v-debounce="handleClick">Submit</button>3132// 3. 图片懒加载指令33app.directive('lazy', {34 mounted(el, binding) {35 const observer = new IntersectionObserver(([entry]) => {36 if (entry.isIntersecting) {37 el.src = binding.value38 observer.unobserve(el)39 }40 })41 observer.observe(el)42 }43})4445// 使用46<img v-lazy="imageUrl" />五、Computed 与 Watch
5.1 computed 和 watch 的区别
答案:
| 特性 | computed | watch |
|---|---|---|
| 用途 | 计算派生数据 | 监听数据变化执行操作 |
| 缓存 | 有缓存,依赖不变不重新计算 | 无缓存 |
| 返回值 | 必须有返回值 | 无返回值 |
| 异步 | 不支持异步 | 支持异步 |
| 使用场景 | 数据转换、过滤、计算 | API 调用、复杂逻辑 |
1export default {2 data() {3 return {4 firstName: 'John',5 lastName: 'Doe',6 searchQuery: ''7 }8 },9 10 // computed:计算属性11 computed: {12 // 简单计算13 fullName() {14 return `${this.firstName} ${this.lastName}`15 },16 17 // getter 和 setter18 fullNameWithSetter: {19 get() {20 return `${this.firstName} ${this.lastName}`21 },22 set(value) {23 const names = value.split(' ')24 this.firstName = names[0]25 this.lastName = names[1]26 }27 }28 },29 30 // watch:侦听器31 watch: {32 // 简单侦听33 searchQuery(newVal, oldVal) {34 console.log(`Search changed from ${oldVal} to ${newVal}`)35 this.fetchResults(newVal)36 },37 38 // 深度侦听39 user: {40 handler(newVal, oldVal) {41 console.log('User changed')42 },43 deep: true,44 immediate: true45 },46 47 // 侦听对象属性48 'user.name'(newVal) {49 console.log('User name changed:', newVal)50 }51 },52 53 methods: {54 async fetchResults(query) {55 // 异步操作56 const results = await api.search(query)57 this.results = results58 }59 }60}Vue 3 Composition API:
1import { ref, computed, watch, watchEffect } from 'vue'23export default {4 setup() {5 const firstName = ref('John')6 const lastName = ref('Doe')7 const searchQuery = ref('')8 9 // computed10 const fullName = computed(() => {11 return `${firstName.value} ${lastName.value}`12 })13 14 // computed with setter15 const fullNameWithSetter = computed({16 get: () => `${firstName.value} ${lastName.value}`,17 set: (value) => {18 const names = value.split(' ')19 firstName.value = names[0]20 lastName.value = names[1]21 }22 })23 24 // watch25 watch(searchQuery, (newVal, oldVal) => {26 console.log(`Search changed from ${oldVal} to ${newVal}`)27 })28 29 // watch 多个源30 watch([firstName, lastName], ([newFirst, newLast]) => {31 console.log(`Name: ${newFirst} ${newLast}`)32 })33 34 // watchEffect:自动追踪依赖35 watchEffect(() => {36 console.log(`Full name: ${firstName.value} ${lastName.value}`)37 })38 39 return {40 firstName,41 lastName,42 fullName,43 searchQuery44 }45 }46}六、虚拟 DOM 与 Diff 算法
6.1 什么是虚拟 DOM?为什么需要虚拟 DOM?
答案:
虚拟 DOM(Virtual DOM) 是用 JavaScript 对象描述真实 DOM 的一种数据结构。
为什么需要虚拟 DOM:
- 性能优化:减少直接操作 DOM,批量更新
- 跨平台:可以渲染到不同平台(Web、Native、SSR)
- 开发体验:声明式编程,不需要手动操作 DOM
1// 虚拟 DOM 示例2const vnode = {3 tag: 'div',4 props: {5 class: 'container',6 id: 'app'7 },8 children: [9 {10 tag: 'h1',11 props: {},12 children: 'Hello World'13 },14 {15 tag: 'p',16 props: { class: 'text' },17 children: 'This is a paragraph'18 }19 ]20}2122// 对应的真实 DOM23<div class="container" id="app">24 <h1>Hello World</h1>25 <p class="text">This is a paragraph</p>26</div>6.2 Vue 的 Diff 算法原理
答案:
Vue 使用 同层比较 的 Diff 算法,时间复杂度为 O(n)。
核心策略:
- 同层比较:只比较同一层级的节点
- 类型判断:不同类型直接替换
- key 优化:使用 key 提高复用率
- 双端比较:从两端向中间比较
Diff 流程:
1// 简化的 Diff 算法2function patch(oldVNode, newVNode) {3 // 1. 节点类型不同,直接替换4 if (oldVNode.tag !== newVNode.tag) {5 return replaceNode(oldVNode, newVNode)6 }7 8 // 2. 文本节点9 if (isTextNode(newVNode)) {10 if (oldVNode.text !== newVNode.text) {11 return updateText(oldVNode, newVNode)12 }13 return14 }15 16 // 3. 更新属性17 updateProps(oldVNode, newVNode)18 19 // 4. 更新子节点20 updateChildren(oldVNode.children, newVNode.children)21}2223// 双端比较算法24function updateChildren(oldChildren, newChildren) {25 let oldStartIdx = 026 let oldEndIdx = oldChildren.length - 127 let newStartIdx = 028 let newEndIdx = newChildren.length - 129 30 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {31 // 1. 旧开始 vs 新开始32 if (sameVNode(oldChildren[oldStartIdx], newChildren[newStartIdx])) {33 patch(oldChildren[oldStartIdx], newChildren[newStartIdx])34 oldStartIdx++35 newStartIdx++36 }37 // 2. 旧结束 vs 新结束38 else if (sameVNode(oldChildren[oldEndIdx], newChildren[newEndIdx])) {39 patch(oldChildren[oldEndIdx], newChildren[newEndIdx])40 oldEndIdx--41 newEndIdx--42 }43 // 3. 旧开始 vs 新结束44 else if (sameVNode(oldChildren[oldStartIdx], newChildren[newEndIdx])) {45 patch(oldChildren[oldStartIdx], newChildren[newEndIdx])46 // 移动节点47 oldStartIdx++48 newEndIdx--49 }50 // 4. 旧结束 vs 新开始51 else if (sameVNode(oldChildren[oldEndIdx], newChildren[newStartIdx])) {52 patch(oldChildren[oldEndIdx], newChildren[newStartIdx])53 // 移动节点54 oldEndIdx--55 newStartIdx++56 }57 // 5. 使用 key 查找58 else {59 // 在旧节点中查找相同 key 的节点60 const idxInOld = findIdxInOld(newChildren[newStartIdx], oldChildren)61 if (idxInOld) {62 patch(oldChildren[idxInOld], newChildren[newStartIdx])63 // 移动节点64 } else {65 // 创建新节点66 createNode(newChildren[newStartIdx])67 }68 newStartIdx++69 }70 }71 72 // 处理剩余节点73 if (oldStartIdx > oldEndIdx) {74 // 添加新节点75 addNodes(newChildren, newStartIdx, newEndIdx)76 } else {77 // 删除旧节点78 removeNodes(oldChildren, oldStartIdx, oldEndIdx)79 }80}为什么需要 key:
1<!-- 不使用 key -->2<div v-for="item in items">{{ item.name }}</div>34<!-- 使用 key -->5<div v-for="item in items" :key="item.id">{{ item.name }}</div>使用 key 的好处:
- 提高 Diff 效率
- 避免就地复用导致的问题
- 保持组件状态
七、Vue Router
7.1 Vue Router 的路由模式有哪些?
答案:
1. Hash 模式(默认)
- URL 格式:
http://example.com/#/user/123 - 原理:监听
hashchange事件 - 优点:兼容性好,不需要服务器配置
- 缺点:URL 不美观,SEO 不友好
1import { createRouter, createWebHashHistory } from 'vue-router'23const router = createRouter({4 history: createWebHashHistory(),5 routes: [...]6})2. History 模式
- URL 格式:
http://example.com/user/123 - 原理:使用 HTML5 History API(pushState、replaceState)
- 优点:URL 美观,SEO 友好
- 缺点:需要服务器配置,刷新会 404
1import { createRouter, createWebHistory } from 'vue-router'23const router = createRouter({4 history: createWebHistory(),5 routes: [...]6})服务器配置(Nginx):
1location / {2 try_files $uri $uri/ /index.html;3}3. Memory 模式(SSR)
1import { createRouter, createMemoryHistory } from 'vue-router'23const router = createRouter({4 history: createMemoryHistory(),5 routes: [...]6})7.2 路由守卫有哪些?如何使用?
答案:
全局守卫:
1// 全局前置守卫2router.beforeEach((to, from, next) => {3 // 权限验证4 if (to.meta.requiresAuth && !isAuthenticated()) {5 next('/login')6 } else {7 next()8 }9})1011// 全局解析守卫12router.beforeResolve((to, from, next) => {13 // 在导航被确认之前,所有组件内守卫和异步路由组件被解析之后调用14 next()15})1617// 全局后置钩子18router.afterEach((to, from) => {19 // 不接受 next 函数20 // 修改页面标题21 document.title = to.meta.title || 'Default Title'22})路由独享守卫:
1const routes = [2 {3 path: '/admin',4 component: Admin,5 beforeEnter: (to, from, next) => {6 if (hasAdminPermission()) {7 next()8 } else {9 next('/403')10 }11 }12 }13]组件内守卫:
1export default {2 // 进入组件前3 beforeRouteEnter(to, from, next) {4 // 不能访问 this,因为组件实例还未创建5 next(vm => {6 // 通过 vm 访问组件实例7 })8 },9 10 // 路由更新时11 beforeRouteUpdate(to, from, next) {12 // 可以访问 this13 // 例如:/user/1 -> /user/214 this.fetchData(to.params.id)15 next()16 },17 18 // 离开组件时19 beforeRouteLeave(to, from, next) {20 // 可以访问 this21 const answer = window.confirm('确定要离开吗?未保存的更改将丢失。')22 if (answer) {23 next()24 } else {25 next(false)26 }27 }28}完整的导航解析流程:
- 导航被触发
- 在失活的组件里调用
beforeRouteLeave - 调用全局的
beforeEach - 在重用的组件里调用
beforeRouteUpdate - 在路由配置里调用
beforeEnter - 解析异步路由组件
- 在被激活的组件里调用
beforeRouteEnter - 调用全局的
beforeResolve - 导航被确认
- 调用全局的
afterEach - 触发 DOM 更新
- 调用
beforeRouteEnter中传给next的回调函数
八、Vuex / Pinia
8.1 Vuex 的核心概念
答案:
Vuex 核心概念:
- State:单一状态树
- Getters:计算属性
- Mutations:同步修改状态
- Actions:异步操作
- Modules:模块化
1// store/index.js2import { createStore } from 'vuex'34const store = createStore({5 // 1. State6 state: {7 count: 0,8 user: null,9 todos: []10 },11 12 // 2. Getters13 getters: {14 doubleCount: state => state.count * 2,15 completedTodos: state => {16 return state.todos.filter(todo => todo.completed)17 },18 getTodoById: state => id => {19 return state.todos.find(todo => todo.id === id)20 }21 },22 23 // 3. Mutations(同步)24 mutations: {25 INCREMENT(state) {26 state.count++27 },28 SET_USER(state, user) {29 state.user = user30 },31 ADD_TODO(state, todo) {32 state.todos.push(todo)33 }34 },35 36 // 4. Actions(异步)37 actions: {38 async fetchUser({ commit }, userId) {39 const user = await api.getUser(userId)40 commit('SET_USER', user)41 },42 async addTodo({ commit }, todo) {43 const newTodo = await api.createTodo(todo)44 commit('ADD_TODO', newTodo)45 }46 },47 48 // 5. Modules49 modules: {50 user: {51 namespaced: true,52 state: () => ({ ... }),53 mutations: { ... },54 actions: { ... }55 }56 }57})5859export default store在组件中使用:
1<template>2 <div>3 <p>Count: {{ count }}</p>4 <p>Double: {{ doubleCount }}</p>5 <button @click="increment">Increment</button>6 <button @click="fetchUser">Fetch User</button>7 </div>8</template>910<script>11import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'1213export default {14 computed: {15 // 方式1:直接访问16 count() {17 return this.$store.state.count18 },19 20 // 方式2:使用辅助函数21 ...mapState(['count', 'user']),22 ...mapGetters(['doubleCount', 'completedTodos'])23 },24 25 methods: {26 // 方式1:直接调用27 increment() {28 this.$store.commit('INCREMENT')29 },30 fetchUser() {31 this.$store.dispatch('fetchUser', 123)32 },33 34 // 方式2:使用辅助函数35 ...mapMutations(['INCREMENT', 'SET_USER']),36 ...mapActions(['fetchUser', 'addTodo'])37 }38}39</script>8.2 Pinia 相比 Vuex 的优势
答案:
Pinia 优势:
- 更简单的 API,去除了 mutations
- 完整的 TypeScript 支持
- 更好的代码分割
- 没有嵌套的模块结构
- 支持 Vue 2 和 Vue 3
1// stores/user.js2import { defineStore } from 'pinia'34export const useUserStore = defineStore('user', {5 // State6 state: () => ({7 name: 'John',8 age: 30,9 todos: []10 }),11 12 // Getters13 getters: {14 doubleAge: (state) => state.age * 2,15 completedTodos: (state) => {16 return state.todos.filter(todo => todo.completed)17 }18 },19 20 // Actions(同步和异步)21 actions: {22 updateName(newName) {23 this.name = newName24 },25 26 async fetchTodos() {27 this.todos = await api.getTodos()28 },29 30 async addTodo(todo) {31 const newTodo = await api.createTodo(todo)32 this.todos.push(newTodo)33 }34 }35})在组件中使用:
1<template>2 <div>3 <p>Name: {{ userStore.name }}</p>4 <p>Age: {{ userStore.doubleAge }}</p>5 <button @click="userStore.updateName('Jane')">Update Name</button>6 </div>7</template>89<script setup>10import { useUserStore } from '@/stores/user'11import { storeToRefs } from 'pinia'1213const userStore = useUserStore()1415// 解构保持响应式16const { name, age } = storeToRefs(userStore)17const { updateName, fetchTodos } = userStore18</script>九、性能优化
9.1 Vue 性能优化的方法有哪些?
答案:
1. 代码层面优化:
1// ① v-if vs v-show2// v-if:条件渲染,切换开销大3// v-show:CSS 显示隐藏,初始渲染开销大4<div v-if="isShow">Conditional</div> // 频繁切换用 v-show5<div v-show="isShow">Toggle</div> // 初始不显示用 v-if67// ② 使用 key8<div v-for="item in items" :key="item.id">{{ item.name }}</div>910// ③ 路由懒加载11const Home = () => import('./views/Home.vue')12const About = () => import('./views/About.vue')1314// ④ 组件懒加载15components: {16 AsyncComponent: () => import('./AsyncComponent.vue')17}1819// ⑤ keep-alive 缓存组件20<keep-alive :include="['Home', 'About']">21 <router-view />22</keep-alive>2324// ⑥ 长列表优化 - 虚拟滚动25import VirtualList from 'vue-virtual-scroll-list'2627// ⑦ 事件销毁28beforeUnmount() {29 window.removeEventListener('resize', this.handleResize)30 clearInterval(this.timer)31}3233// ⑧ 图片懒加载34<img v-lazy="imageUrl" />3536// ⑨ 第三方插件按需引入37import { Button, Select } from 'element-plus'3839// ⑩ 使用函数式组件(无状态组件)40export default {41 functional: true,42 render(h, context) {43 return h('div', context.props.text)44 }45}2. 打包优化:
1// vite.config.js2export default {3 build: {4 // 代码分割5 rollupOptions: {6 output: {7 manualChunks: {8 'vue-vendor': ['vue', 'vue-router', 'pinia'],9 'ui-vendor': ['element-plus']10 }11 }12 },13 // 压缩14 minify: 'terser',15 terserOptions: {16 compress: {17 drop_console: true,18 drop_debugger: true19 }20 }21 }22}3. 使用 computed 缓存:
1// ❌ 不好的做法2<div>{{ getFullName() }}</div>34methods: {5 getFullName() {6 return this.firstName + ' ' + this.lastName7 }8}910// ✅ 好的做法11<div>{{ fullName }}</div>1213computed: {14 fullName() {15 return this.firstName + ' ' + this.lastName16 }17}4. 使用 Object.freeze 冻结数据:
1export default {2 data() {3 return {4 // 大量不需要响应式的数据5 largeList: Object.freeze([...])6 }7 }8}5. 使用 v-once 和 v-memo:
1<!-- 只渲染一次 -->2<div v-once>{{ staticContent }}</div>34<!-- Vue 3: 条件缓存 -->5<div v-memo="[item.id, item.name]">6 {{ item.name }}7</div>十、常见问题
10.1 Vue 中的 $nextTick 是什么?
答案:
$nextTick 在下次 DOM 更新循环结束之后执行延迟回调。
使用场景:
- 在数据变化后立即操作更新后的 DOM
- 在 created 钩子中操作 DOM
1export default {2 data() {3 return {4 message: 'Hello'5 }6 },7 methods: {8 updateMessage() {9 this.message = 'Updated'10 11 // 此时 DOM 还未更新12 console.log(this.$refs.msg.textContent) // 'Hello'13 14 // 等待 DOM 更新后执行15 this.$nextTick(() => {16 console.log(this.$refs.msg.textContent) // 'Updated'17 })18 19 // Vue 3 也可以使用 Promise20 this.$nextTick().then(() => {21 console.log(this.$refs.msg.textContent) // 'Updated'22 })23 }24 }25}原理:
Vue 使用异步队列来批量更新 DOM,优先使用微任务(Promise、MutationObserver),降级使用宏任务(setTimeout)。
10.2 Vue 中如何实现组件的递归?
答案:
组件可以在自己的模板中递归调用自己,需要设置 name 选项。
1<!-- TreeNode.vue -->2<template>3 <div class="tree-node">4 <div class="node-content">5 <span @click="toggle">{{ node.name }}</span>6 </div>7 8 <div v-if="isExpanded && node.children" class="children">9 <!-- 递归调用自己 -->10 <TreeNode11 v-for="child in node.children"12 :key="child.id"13 :node="child"14 />15 </div>16 </div>17</template>1819<script>20export default {21 name: 'TreeNode', // 必须设置 name22 props: {23 node: {24 type: Object,25 required: true26 }27 },28 data() {29 return {30 isExpanded: false31 }32 },33 methods: {34 toggle() {35 this.isExpanded = !this.isExpanded36 }37 }38}39</script>4041<!-- 使用 -->42<template>43 <TreeNode :node="treeData" />44</template>4546<script>47export default {48 data() {49 return {50 treeData: {51 id: 1,52 name: 'Root',53 children: [54 {55 id: 2,56 name: 'Child 1',57 children: [58 { id: 3, name: 'Grandchild 1' },59 { id: 4, name: 'Grandchild 2' }60 ]61 },62 {63 id: 5,64 name: 'Child 2'65 }66 ]67 }68 }69 }70}71</script>10.3 Vue 中的 slot 插槽如何使用?
答案:
1. 默认插槽:
1<!-- 子组件 Card.vue -->2<template>3 <div class="card">4 <slot>默认内容</slot>5 </div>6</template>78<!-- 父组件使用 -->9<Card>10 <p>自定义内容</p>11</Card>2. 具名插槽:
1<!-- 子组件 Layout.vue -->2<template>3 <div class="layout">4 <header>5 <slot name="header"></slot>6 </header>7 <main>8 <slot></slot>9 </main>10 <footer>11 <slot name="footer"></slot>12 </footer>13 </div>14</template>1516<!-- 父组件使用 -->17<Layout>18 <template #header>19 <h1>页面标题</h1>20 </template>21 22 <p>主要内容</p>23 24 <template #footer>25 <p>页脚信息</p>26 </template>27</Layout>3. 作用域插槽:
1<!-- 子组件 List.vue -->2<template>3 <ul>4 <li v-for="item in items" :key="item.id">5 <slot :item="item" :index="index">6 {{ item.name }}7 </slot>8 </li>9 </ul>10</template>1112<!-- 父组件使用 -->13<List :items="users">14 <template #default="{ item, index }">15 <span>{{ index + 1 }}. {{ item.name }} - {{ item.email }}</span>16 </template>17</List>10.4 Vue 中如何实现权限控制?
答案:
1. 路由级别权限控制:
1// router/index.js2const routes = [3 {4 path: '/admin',5 component: Admin,6 meta: { requiresAuth: true, roles: ['admin'] }7 }8]910// 路由守卫11router.beforeEach((to, from, next) => {12 const user = store.state.user13 14 if (to.meta.requiresAuth) {15 if (!user) {16 next('/login')17 } else if (to.meta.roles && !to.meta.roles.includes(user.role)) {18 next('/403')19 } else {20 next()21 }22 } else {23 next()24 }25})2. 按钮级别权限控制:
1// directives/permission.js2export default {3 mounted(el, binding) {4 const { value } = binding5 const permissions = store.state.user.permissions6 7 if (value && !permissions.includes(value)) {8 el.parentNode?.removeChild(el)9 }10 }11}1213// 使用14<button v-permission="'user:delete'">删除</button>3. 组件级别权限控制:
1<!-- Permission.vue -->2<template>3 <div v-if="hasPermission">4 <slot></slot>5 </div>6</template>78<script>9export default {10 props: ['permission'],11 computed: {12 hasPermission() {13 const permissions = this.$store.state.user.permissions14 return permissions.includes(this.permission)15 }16 }17}18</script>1920<!-- 使用 -->21<Permission permission="user:edit">22 <button>编辑</button>23</Permission>十一、前后台交互
11.1 Vue 中的异步请求方式
面试回答思路:
Vue 本身不提供 HTTP 请求功能,需要使用第三方库或原生 API。主要有三种方式:fetch、axios 和 async/await。
1. Fetch API(原生)
Fetch 是浏览器原生提供的 API,基于 Promise。
基本使用:
1// GET 请求2fetch('https://api.example.com/users')3 .then(response => {4 if (!response.ok) {5 throw new Error('Network response was not ok')6 }7 return response.json()8 })9 .then(data => {10 console.log(data)11 })12 .catch(error => {13 console.error('Error:', error)14 })1516// POST 请求17fetch('https://api.example.com/users', {18 method: 'POST',19 headers: {20 'Content-Type': 'application/json',21 },22 body: JSON.stringify({23 name: 'John',24 email: 'john@example.com'25 })26})27 .then(response => response.json())28 .then(data => console.log(data))29 .catch(error => console.error('Error:', error))在 Vue 组件中使用:
1<template>2 <div>3 <ul v-if="users.length">4 <li v-for="user in users" :key="user.id">5 {{ user.name }}6 </li>7 </ul>8 <p v-else-if="loading">Loading...</p>9 <p v-else-if="error">{{ error }}</p>10 </div>11</template>1213<script>14export default {15 data() {16 return {17 users: [],18 loading: false,19 error: null20 }21 },22 23 mounted() {24 this.fetchUsers()25 },26 27 methods: {28 fetchUsers() {29 this.loading = true30 this.error = null31 32 fetch('https://api.example.com/users')33 .then(response => {34 if (!response.ok) {35 throw new Error(`HTTP error! status: ${response.status}`)36 }37 return response.json()38 })39 .then(data => {40 this.users = data41 this.loading = false42 })43 .catch(error => {44 this.error = error.message45 this.loading = false46 })47 }48 }49}50</script>Fetch 的优缺点:
优点:
- 原生 API,不需要额外安装
- 基于 Promise,支持 async/await
- 更现代的 API 设计
缺点:
- 不支持请求取消(需要 AbortController)
- 不支持请求超时
- 不支持上传进度
- 默认不携带 cookie(需要设置 credentials)
- 错误处理不够友好(404、500 不会 reject)
- 不支持拦截器
11.2 Axios 的使用和封装
面试回答思路:
Axios 是目前 Vue 项目中最常用的 HTTP 库,功能强大且易用。
为什么选择 Axios:
- 功能丰富:支持请求/响应拦截、取消请求、超时设置等
- 自动转换:自动转换 JSON 数据
- 错误处理:更好的错误处理机制
- 浏览器兼容:支持老版本浏览器
- 防御 XSRF:内置 CSRF 防护
- 进度监控:支持上传/下载进度
基本使用:
1import axios from 'axios'23// GET 请求4axios.get('https://api.example.com/users')5 .then(response => {6 console.log(response.data)7 })8 .catch(error => {9 console.error(error)10 })1112// POST 请求13axios.post('https://api.example.com/users', {14 name: 'John',15 email: 'john@example.com'16})17 .then(response => {18 console.log(response.data)19 })20 .catch(error => {21 console.error(error)22 })2324// 并发请求25axios.all([26 axios.get('https://api.example.com/users'),27 axios.get('https://api.example.com/posts')28])29 .then(axios.spread((users, posts) => {30 console.log(users.data, posts.data)31 }))在 Vue 中封装 Axios:
1// api/request.js2import axios from 'axios'3import { ElMessage } from 'element-plus'4import router from '@/router'56// 创建 axios 实例7const service = axios.create({8 baseURL: process.env.VUE_APP_BASE_API, // API 基础路径9 timeout: 10000, // 请求超时时间10 headers: {11 'Content-Type': 'application/json;charset=UTF-8'12 }13})1415// 请求拦截器16service.interceptors.request.use(17 config => {18 // 在发送请求之前做些什么19 20 // 1. 添加 token21 const token = localStorage.getItem('token')22 if (token) {23 config.headers['Authorization'] = `Bearer ${token}`24 }25 26 // 2. 显示 loading27 // showLoading()28 29 // 3. 请求参数处理30 if (config.method === 'get') {31 // GET 请求参数序列化32 config.params = {33 ...config.params,34 _t: Date.now() // 防止缓存35 }36 }37 38 return config39 },40 error => {41 // 请求错误处理42 console.error('Request error:', error)43 return Promise.reject(error)44 }45)4647// 响应拦截器48service.interceptors.response.use(49 response => {50 // hideLoading()51 52 const res = response.data53 54 // 根据后端约定的状态码判断55 if (res.code !== 200) {56 ElMessage.error(res.message || 'Error')57 58 // 特殊状态码处理59 if (res.code === 401) {60 // token 过期,跳转登录61 ElMessage.error('登录已过期,请重新登录')62 localStorage.removeItem('token')63 router.push('/login')64 }65 66 return Promise.reject(new Error(res.message || 'Error'))67 }68 69 return res.data70 },71 error => {72 // hideLoading()73 74 console.error('Response error:', error)75 76 // 处理不同的错误状态77 if (error.response) {78 switch (error.response.status) {79 case 400:80 ElMessage.error('请求参数错误')81 break82 case 401:83 ElMessage.error('未授权,请重新登录')84 localStorage.removeItem('token')85 router.push('/login')86 break87 case 403:88 ElMessage.error('拒绝访问')89 break90 case 404:91 ElMessage.error('请求地址不存在')92 break93 case 500:94 ElMessage.error('服务器错误')95 break96 default:97 ElMessage.error(error.response.data.message || '请求失败')98 }99 } else if (error.request) {100 ElMessage.error('网络错误,请检查网络连接')101 } else {102 ElMessage.error('请求配置错误')103 }104 105 return Promise.reject(error)106 }107)108109export default service封装 API 接口:
1// api/user.js2import request from './request'34// 用户相关 API5export const userApi = {6 // 获取用户列表7 getUsers(params) {8 return request({9 url: '/users',10 method: 'get',11 params12 })13 },14 15 // 获取用户详情16 getUserById(id) {17 return request({18 url: `/users/${id}`,19 method: 'get'20 })21 },22 23 // 创建用户24 createUser(data) {25 return request({26 url: '/users',27 method: 'post',28 data29 })30 },31 32 // 更新用户33 updateUser(id, data) {34 return request({35 url: `/users/${id}`,36 method: 'put',37 data38 })39 },40 41 // 删除用户42 deleteUser(id) {43 return request({44 url: `/users/${id}`,45 method: 'delete'46 })47 },48 49 // 上传文件50 uploadFile(file, onProgress) {51 const formData = new FormData()52 formData.append('file', file)53 54 return request({55 url: '/upload',56 method: 'post',57 data: formData,58 headers: {59 'Content-Type': 'multipart/form-data'60 },61 onUploadProgress: progressEvent => {62 if (onProgress) {63 const percentCompleted = Math.round(64 (progressEvent.loaded * 100) / progressEvent.total65 )66 onProgress(percentCompleted)67 }68 }69 })70 }71}在组件中使用:
1<template>2 <div>3 <ul v-if="users.length">4 <li v-for="user in users" :key="user.id">5 {{ user.name }}6 </li>7 </ul>8 <button @click="loadMore">加载更多</button>9 </div>10</template>1112<script>13import { userApi } from '@/api/user'1415export default {16 data() {17 return {18 users: [],19 loading: false,20 page: 1,21 pageSize: 1022 }23 },24 25 mounted() {26 this.fetchUsers()27 },28 29 methods: {30 async fetchUsers() {31 try {32 this.loading = true33 const data = await userApi.getUsers({34 page: this.page,35 pageSize: this.pageSize36 })37 this.users = data.list38 } catch (error) {39 console.error('获取用户列表失败:', error)40 } finally {41 this.loading = false42 }43 },44 45 async loadMore() {46 this.page++47 try {48 const data = await userApi.getUsers({49 page: this.page,50 pageSize: this.pageSize51 })52 this.users.push(...data.list)53 } catch (error) {54 this.page--55 }56 }57 }58}59</script>11.3 async/await 的使用
面试回答思路:
async/await 是 ES2017 引入的异步编程语法糖,让异步代码看起来像同步代码。
基本概念:
async 函数:
- 返回一个 Promise
- 可以使用 await 关键字
- 自动将返回值包装成 Promise
await 关键字:
- 只能在 async 函数中使用
- 等待 Promise 完成
- 返回 Promise 的结果
基本使用:
1// 传统 Promise 写法2function fetchUser() {3 return fetch('/api/user')4 .then(response => response.json())5 .then(user => {6 return fetch(`/api/posts?userId=${user.id}`)7 })8 .then(response => response.json())9 .then(posts => {10 console.log(posts)11 })12 .catch(error => {13 console.error(error)14 })15}1617// async/await 写法18async function fetchUser() {19 try {20 const response = await fetch('/api/user')21 const user = await response.json()22 23 const postsResponse = await fetch(`/api/posts?userId=${user.id}`)24 const posts = await postsResponse.json()25 26 console.log(posts)27 } catch (error) {28 console.error(error)29 }30}在 Vue 中使用:
1<script>2export default {3 data() {4 return {5 user: null,6 posts: [],7 loading: false,8 error: null9 }10 },11 12 async mounted() {13 await this.fetchData()14 },15 16 methods: {17 // 方法 1:async 方法18 async fetchData() {19 this.loading = true20 this.error = null21 22 try {23 // 串行请求24 const user = await userApi.getUserById(1)25 this.user = user26 27 const posts = await postApi.getPostsByUserId(user.id)28 this.posts = posts29 } catch (error) {30 this.error = error.message31 console.error('获取数据失败:', error)32 } finally {33 this.loading = false34 }35 },36 37 // 方法 2:并行请求38 async fetchDataParallel() {39 this.loading = true40 41 try {42 // 使用 Promise.all 并行请求43 const [user, posts] = await Promise.all([44 userApi.getUserById(1),45 postApi.getPosts()46 ])47 48 this.user = user49 this.posts = posts50 } catch (error) {51 this.error = error.message52 } finally {53 this.loading = false54 }55 },56 57 // 方法 3:错误处理58 async createUser(userData) {59 try {60 const user = await userApi.createUser(userData)61 this.$message.success('创建成功')62 return user63 } catch (error) {64 if (error.response?.status === 400) {65 this.$message.error('参数错误')66 } else if (error.response?.status === 409) {67 this.$message.error('用户已存在')68 } else {69 this.$message.error('创建失败')70 }71 throw error72 }73 },74 75 // 方法 4:循环中使用76 async processUsers(userIds) {77 const results = []78 79 // 串行处理80 for (const id of userIds) {81 const user = await userApi.getUserById(id)82 results.push(user)83 }84 85 // 并行处理(推荐)86 const users = await Promise.all(87 userIds.map(id => userApi.getUserById(id))88 )89 90 return users91 }92 }93}94</script>Vue 3 Composition API 中使用:
1<script setup>2import { ref, onMounted } from 'vue'3import { userApi } from '@/api/user'45const users = ref([])6const loading = ref(false)7const error = ref(null)89// 方式 1:在 onMounted 中使用10onMounted(async () => {11 await fetchUsers()12})1314// 方式 2:定义 async 函数15const fetchUsers = async () => {16 loading.value = true17 error.value = null18 19 try {20 const data = await userApi.getUsers()21 users.value = data22 } catch (err) {23 error.value = err.message24 } finally {25 loading.value = false26 }27}2829// 方式 3:使用 VueUse 的 useAsyncState30import { useAsyncState } from '@vueuse/core'3132const { state, isLoading, error, execute } = useAsyncState(33 () => userApi.getUsers(),34 [],35 { immediate: true }36)37</script>最佳实践:
1. 错误处理
1// ✅ 好的做法:使用 try-catch2async function fetchData() {3 try {4 const data = await api.getData()5 return data6 } catch (error) {7 console.error('Error:', error)8 throw error // 或者返回默认值9 }10}1112// ❌ 不好的做法:不处理错误13async function fetchData() {14 const data = await api.getData() // 如果失败会导致未捕获的错误15 return data16}2. 并行 vs 串行
1// ❌ 串行(慢)2async function fetchAll() {3 const users = await userApi.getUsers()4 const posts = await postApi.getPosts()5 return { users, posts }6}78// ✅ 并行(快)9async function fetchAll() {10 const [users, posts] = await Promise.all([11 userApi.getUsers(),12 postApi.getPosts()13 ])14 return { users, posts }15}3. 避免在循环中使用 await
1// ❌ 不好:串行执行2async function processUsers(ids) {3 const results = []4 for (const id of ids) {5 const user = await userApi.getUserById(id)6 results.push(user)7 }8 return results9}1011// ✅ 好:并行执行12async function processUsers(ids) {13 return await Promise.all(14 ids.map(id => userApi.getUserById(id))15 )16}4. 超时处理
1function timeout(ms) {2 return new Promise((_, reject) => {3 setTimeout(() => reject(new Error('Timeout')), ms)4 })5}67async function fetchWithTimeout(url, ms = 5000) {8 try {9 const data = await Promise.race([10 fetch(url),11 timeout(ms)12 ])13 return data14 } catch (error) {15 console.error('请求超时或失败:', error)16 throw error17 }18}5. 取消请求
1// 使用 AbortController2const controller = new AbortController()34async function fetchData() {5 try {6 const response = await fetch('/api/data', {7 signal: controller.signal8 })9 return await response.json()10 } catch (error) {11 if (error.name === 'AbortError') {12 console.log('请求已取消')13 } else {14 console.error('请求失败:', error)15 }16 }17}1819// 取消请求20controller.abort()11.4 三种方式的对比和选择
面试回答思路:
| 特性 | Fetch | Axios | async/await |
|---|---|---|---|
| 类型 | 原生 API | 第三方库 | 语法糖 |
| 浏览器支持 | 现代浏览器 | 所有浏览器 | ES2017+ |
| 包大小 | 0(原生) | ~13KB | 0(语法) |
| 自动转换 JSON | ❌ 需手动 | ✅ 自动 | 取决于使用的库 |
| 拦截器 | ❌ | ✅ | 取决于使用的库 |
| 取消请求 | ✅ AbortController | ✅ CancelToken | 取决于使用的库 |
| 上传进度 | ❌ | ✅ | 取决于使用的库 |
| 超时设置 | ❌ | ✅ | 需手动实现 |
| 错误处理 | 较复杂 | 简单 | 简单 |
| CSRF 防护 | ❌ | ✅ | 取决于使用的库 |
选择建议:
- 小型项目或简单需求:使用 Fetch + async/await
- 中大型项目:使用 Axios + async/await(推荐)
- 需要兼容老浏览器:使用 Axios
- 需要拦截器、进度监控等高级功能:使用 Axios
实际项目中的最佳实践:
1// 推荐组合:Axios + async/await + 统一封装23// 1. 封装 axios 实例4// 2. 配置拦截器5// 3. 封装 API 接口6// 4. 在组件中使用 async/await 调用78// 这样可以获得:9// - Axios 的强大功能10// - async/await 的简洁语法11// - 统一的错误处理12// - 良好的代码组织十二、常见面试题补充
12.1 v-if 和 v-show 的区别
面试回答思路:
v-if 和 v-show 都可以控制元素的显示和隐藏,但它们的实现原理和使用场景完全不同。这是 Vue 面试中的高频考点,需要从原理、性能、使用场景等多个角度来回答。
v-if vs v-show = 条件渲染 vs CSS 切换
- 🎯 v-if:真正的条件渲染,DOM 元素的创建和销毁
- 🎨 v-show:CSS display 属性切换,元素始终存在
- ⚡ 性能:v-if 切换开销大,v-show 初始渲染开销大
- 🔄 生命周期:v-if 触发生命周期,v-show 不触发
实现原理对比:
- v-if 原理
- v-show 原理
v-if 是真正的条件渲染:
- 条件为 false:元素不会被渲染到 DOM 中
- 条件为 true:创建元素并渲染到 DOM
- 切换时:销毁和重建元素及其内部的组件
- 生命周期:触发 mounted、unmounted 等钩子
- 惰性渲染:初始条件为 false 时,什么都不做
1<template>2 <!-- 条件为 false 时,DOM 中不存在这个元素 -->3 <div v-if="isShow">4 <h1>使用 v-if</h1>5 <ExpensiveComponent />6 </div>7</template>89<script>10export default {11 data() {12 return {13 isShow: false // DOM 中不会渲染这个 div14 }15 }16}17</script>编译后的渲染函数(简化):
1function render() {2 return isShow 3 ? h('div', [h('h1', '使用 v-if'), h(ExpensiveComponent)])4 : null // 不渲染任何内容5}v-show 是 CSS 切换:
- 无论条件:元素都会被渲染到 DOM 中
- 切换时:只修改 CSS 的 display 属性
- 元素状态:始终存在,只是视觉上的显示隐藏
- 生命周期:不触发生命周期钩子
- 初始渲染:总是会渲染,初始成本高
1<template>2 <!-- 元素始终存在于 DOM 中,只是 display: none -->3 <div v-show="isVisible">4 <h1>使用 v-show</h1>5 <SimpleComponent />6 </div>7</template>89<script>10export default {11 data() {12 return {13 isVisible: false // DOM 中存在,但 display: none14 }15 }16}17</script>编译后的渲染函数(简化):
1function render() {2 return h('div', {3 style: { display: isVisible ? '' : 'none' }4 }, [h('h1', '使用 v-show'), h(SimpleComponent)])5}性能对比分析:
| 特性 | v-if | v-show |
|---|---|---|
| 渲染方式 | 条件渲染(创建/销毁) | CSS 切换(display) |
| 初始渲染开销 | 低(惰性渲染) | 高(总是渲染) |
| 切换开销 | 高(重建 DOM) | 低(只改 CSS) |
| 生命周期 | ✅ 触发 | ❌ 不触发 |
| v-else 支持 | ✅ 支持 | ❌ 不支持 |
| template 支持 | ✅ 支持 | ❌ 不支持 |
| 适用场景 | 条件很少改变 | 频繁切换 |
| 内存占用 | 低(不渲染时释放) | 高(始终占用) |
频繁切换时使用 v-if 会导致性能问题!
假设一个标签页组件每秒切换 10 次:
- 使用 v-if:每次切换都要销毁和重建组件,触发生命周期,性能开销巨大
- 使用 v-show:只是修改 CSS,性能开销极小
测试数据:1000 次切换
- v-if:约 500ms
- v-show:约 50ms(快 10 倍)
语法支持对比:
- v-if 语法
- v-show 语法
1<template>2 <div>3 <!-- 1. 基本用法 -->4 <div v-if="isShow">显示内容</div>5 6 <!-- 2. v-else-if 和 v-else -->7 <div v-if="type === 'A'">A</div>8 <div v-else-if="type === 'B'">B</div>9 <div v-else>C</div>10 11 <!-- 3. 配合 template(不产生额外 DOM) -->12 <template v-if="showGroup">13 <h1>标题</h1>14 <p>内容</p>15 <button>按钮</button>16 </template>17 18 <!-- 4. 在组件上使用 -->19 <MyComponent v-if="isShow" />20 21 <!-- 5. 多条件判断 -->22 <div v-if="isLoggedIn && hasPermission">23 管理员面板24 </div>25 </div>26</template>1<template>2 <div>3 <!-- 1. 基本用法 -->4 <div v-show="isVisible">显示内容</div>5 6 <!-- 2. ❌ 不支持 v-else -->7 <!-- <div v-show="isVisible">A</div> -->8 <!-- <div v-else>B</div> --> <!-- 错误! -->9 10 <!-- 3. ❌ 不支持 template -->11 <!-- <template v-show="showGroup"> --> <!-- 错误! -->12 <!-- <h1>标题</h1> -->13 <!-- </template> -->14 15 <!-- 4. 在组件上使用(可以,但不推荐) -->16 <MyComponent v-show="isVisible" />17 18 <!-- 5. 多条件判断 -->19 <div v-show="isLoggedIn && hasPermission">20 管理员面板21 </div>22 23 <!-- 6. 正确的替代方案 -->24 <div v-show="isVisible">A</div>25 <div v-show="!isVisible">B</div>26 </div>27</template>使用场景指南:
一句话总结:频繁切换用 v-show,条件很少改变用 v-if
使用 v-if 的场景:
- ✅ 权限控制(不渲染 DOM 更安全)
- ✅ 条件很少改变的情况
- ✅ 需要配合 v-else 使用
- ✅ 初始条件为 false,且可能永远不会变为 true
- ✅ 包含大量子组件或复杂逻辑的元素
- ✅ 需要触发生命周期钩子
使用 v-show 的场景:
- ✅ 标签页切换
- ✅ 模态框/对话框显示隐藏
- ✅ 下拉菜单展开收起
- ✅ 手风琴组件
- ✅ 需要频繁切换显示状态
- ✅ 元素结构简单,渲染成本低
实际案例对比:
- 权限控制
- 标签页切换
- 模态框
- 条件渲染
1<template>2 <div>3 <!-- ✅ 推荐:使用 v-if -->4 <!-- 原因:不渲染 DOM 更安全,用户无法通过开发者工具看到隐藏的按钮 -->5 <button v-if="hasDeletePermission" @click="deleteItem">6 删除7 </button>8 9 <button v-if="hasEditPermission" @click="editItem">10 编辑11 </button>12 13 <!-- ❌ 不推荐:使用 v-show -->14 <!-- 问题:按钮仍在 DOM 中,只是隐藏,用户可以通过开发者工具修改 CSS 看到 -->15 <button v-show="hasDeletePermission" @click="deleteItem">16 删除17 </button>18 </div>19</template>2021<script>22export default {23 computed: {24 hasDeletePermission() {25 return this.$store.state.user.permissions.includes('delete')26 },27 hasEditPermission() {28 return this.$store.state.user.permissions.includes('edit')29 }30 }31}32</script>1<template>2 <div class="tabs">3 <!-- 标签页导航 -->4 <div class="tab-nav">5 <button 6 v-for="tab in tabs" 7 :key="tab.id"8 :class="{ active: activeTab === tab.id }"9 @click="activeTab = tab.id"10 >11 {{ tab.label }}12 </button>13 </div>14 15 <!-- ✅ 推荐:使用 v-show -->16 <!-- 原因:标签页会频繁切换,使用 v-show 性能更好 -->17 <div class="tab-content">18 <div v-show="activeTab === 'home'" class="tab-pane">19 <h2>首页内容</h2>20 <p>这是首页的内容...</p>21 </div>22 23 <div v-show="activeTab === 'profile'" class="tab-pane">24 <h2>个人资料</h2>25 <UserProfile />26 </div>27 28 <div v-show="activeTab === 'settings'" class="tab-pane">29 <h2>设置</h2>30 <Settings />31 </div>32 </div>33 34 <!-- ❌ 不推荐:使用 v-if -->35 <!-- 问题:每次切换都会销毁和重建组件,性能差 -->36 <div class="tab-content">37 <div v-if="activeTab === 'home'">...</div>38 <div v-if="activeTab === 'profile'">...</div>39 <div v-if="activeTab === 'settings'">...</div>40 </div>41 </div>42</template>4344<script>45export default {46 data() {47 return {48 activeTab: 'home',49 tabs: [50 { id: 'home', label: '首页' },51 { id: 'profile', label: '个人资料' },52 { id: 'settings', label: '设置' }53 ]54 }55 }56}57</script>1<template>2 <div>3 <button @click="showModal = true">打开模态框</button>4 5 <!-- ✅ 推荐:使用 v-show -->6 <!-- 原因:模态框会频繁打开关闭,使用 v-show 避免重复渲染 -->7 <div class="modal-overlay" v-show="showModal" @click="showModal = false">8 <div class="modal-content" @click.stop>9 <h2>模态框标题</h2>10 <p>模态框内容...</p>11 <button @click="showModal = false">关闭</button>12 </div>13 </div>14 15 <!-- 💡 特殊情况:如果模态框内容很复杂,可以使用 v-if -->16 <!-- 优点:关闭时释放内存,适合大型表单或复杂组件 -->17 <div class="modal-overlay" v-if="showComplexModal">18 <div class="modal-content">19 <ComplexForm /> <!-- 复杂的表单组件 -->20 </div>21 </div>22 </div>23</template>2425<script>26export default {27 data() {28 return {29 showModal: false,30 showComplexModal: false31 }32 }33}34</script>3536<style scoped>37.modal-overlay {38 position: fixed;39 top: 0;40 left: 0;41 width: 100%;42 height: 100%;43 background: rgba(0, 0, 0, 0.5);44 display: flex;45 align-items: center;46 justify-content: center;47}4849.modal-content {50 background: white;51 padding: 20px;52 border-radius: 8px;53}54</style>1<template>2 <div>3 <!-- ✅ 推荐:使用 v-if -->4 <!-- 原因:根据用户角色渲染不同组件,条件很少改变 -->5 <AdminPanel v-if="userRole === 'admin'" />6 <UserPanel v-else-if="userRole === 'user'" />7 <GuestPanel v-else />8 9 <!-- ✅ 推荐:使用 v-if -->10 <!-- 原因:登录状态改变不频繁 -->11 <div v-if="isLoggedIn">12 <h1>欢迎回来,{{ username }}</h1>13 <button @click="logout">退出登录</button>14 </div>15 <div v-else>16 <h1>请登录</h1>17 <LoginForm />18 </div>19 20 <!-- ✅ 推荐:使用 v-if -->21 <!-- 原因:初始加载时不需要渲染,节省资源 -->22 <HeavyComponent v-if="shouldLoadHeavyComponent" />23 </div>24</template>2526<script>27export default {28 computed: {29 userRole() {30 return this.$store.state.user.role31 },32 isLoggedIn() {33 return this.$store.state.user.isLoggedIn34 }35 }36}37</script>错误 1:在频繁切换的场景使用 v-if
1<!-- ❌ 错误:标签页使用 v-if -->2<div v-if="activeTab === 'home'">首页</div>3<div v-if="activeTab === 'profile'">个人资料</div>4<!-- 问题:每次切换都重建组件,性能差 -->56<!-- ✅ 正确:使用 v-show -->7<div v-show="activeTab === 'home'">首页</div>8<div v-show="activeTab === 'profile'">个人资料</div>错误 2:在权限控制中使用 v-show
1<!-- ❌ 错误:权限控制使用 v-show -->2<button v-show="hasPermission">删除</button>3<!-- 问题:按钮仍在 DOM 中,不安全 -->45<!-- ✅ 正确:使用 v-if -->6<button v-if="hasPermission">删除</button>错误 3:v-show 使用 v-else
1<!-- ❌ 错误:v-show 不支持 v-else -->2<div v-show="isShow">A</div>3<div v-else>B</div> <!-- 不会生效! -->45<!-- ✅ 正确:使用相反条件 -->6<div v-show="isShow">A</div>7<div v-show="!isShow">B</div>总结:
记住这个决策树:
1需要条件渲染?2 ├─ 是否频繁切换?3 │ ├─ 是 → 使用 v-show4 │ └─ 否 → 继续判断5 │6 ├─ 是否涉及权限/安全?7 │ ├─ 是 → 使用 v-if8 │ └─ 否 → 继续判断9 │10 ├─ 是否需要 v-else?11 │ ├─ 是 → 使用 v-if12 │ └─ 否 → 继续判断13 │14 ├─ 初始条件是否为 false?15 │ ├─ 是 → 使用 v-if(惰性渲染)16 │ └─ 否 → 使用 v-show17 │18 └─ 默认 → 使用 v-if(更安全)在实际项目中,我会根据具体场景选择合适的指令,既要考虑性能,也要考虑代码的可读性和维护性。
12.2 v-for 为什么要加 key
面试回答思路:
key 是 Vue 中用于优化列表渲染的重要属性,理解它的作用对于写出高性能的 Vue 应用很重要。这个问题考察的是对 Vue 虚拟 DOM 和 Diff 算法的理解。
key = 节点身份标识 + Diff 优化 + 状态保持
- 🎯 身份标识:帮助 Vue 识别哪些元素是相同的
- ⚡ 性能优化:提高 Diff 算法效率,减少 DOM 操作
- 🔄 状态保持:确保组件状态在列表重排时正确保持
- 🎨 过渡动画:正确触发列表的过渡效果
为什么需要 key?
- 没有 key 的问题
- 使用 key 解决
Vue 的默认行为:就地更新策略
当没有 key 时,Vue 使用"就地更新"策略。数据项顺序改变时,Vue 不会移动 DOM 元素,而是就地更新每个元素的内容。
问题演示:
1<template>2 <div>3 <h3>没有使用 key</h3>4 <div v-for="item in items">5 <input type="checkbox" />6 <span>{{ item.name }}</span>7 </div>8 <button @click="reverse">反转列表</button>9 </div>10</template>1112<script>13export default {14 data() {15 return {16 items: [17 { id: 1, name: 'A' },18 { id: 2, name: 'B' },19 { id: 3, name: 'C' }20 ]21 }22 },23 methods: {24 reverse() {25 this.items.reverse()26 }27 }28}29</script>操作步骤:
- 勾选第一个 checkbox(A)
- 点击"反转列表"按钮
- 结果:checkbox 的选中状态没有跟随数据移动!
原因分析:
1初始状态:2DOM: [✓ A] [ B] [ C]3Data: [ A ] [ B] [ C]45反转后(没有 key):6DOM: [✓ A] [ B] [ C] ← DOM 元素没有移动7Data: [ C ] [ B] [ A] ← 只更新了文本内容89期望结果:10DOM: [ C] [ B] [✓ A] ← checkbox 应该跟随 A 移动Vue 只更新了 <span> 的文本内容,但 checkbox 的状态没有更新,因为 Vue 认为这些是同一个 DOM 元素。
添加唯一的 key:
1<template>2 <div>3 <h3>使用了 key</h3>4 <div v-for="item in items" :key="item.id">5 <input type="checkbox" />6 <span>{{ item.name }}</span>7 </div>8 <button @click="reverse">反转列表</button>9 </div>10</template>1112<script>13export default {14 data() {15 return {16 items: [17 { id: 1, name: 'A' },18 { id: 2, name: 'B' },19 { id: 3, name: 'C' }20 ]21 }22 },23 methods: {24 reverse() {25 this.items.reverse()26 }27 }28}29</script>操作步骤:
- 勾选第一个 checkbox(A)
- 点击"反转列表"按钮
- 结果:checkbox 的选中状态正确跟随 A 移动到最后!
原因分析:
1初始状态:2DOM: [✓ A (key=1)] [ B (key=2)] [ C (key=3)]3Data: [ A (id=1) ] [ B (id=2) ] [ C (id=3) ]45反转后(有 key):6DOM: [ C (key=3)] [ B (key=2)] [✓ A (key=1)] ← DOM 元素移动了7Data: [ C (id=3) ] [ B (id=2) ] [ A (id=1) ]89Vue 通过 key 识别出:10- key=1 的元素从位置 0 移动到位置 211- key=2 的元素保持在位置 112- key=3 的元素从位置 2 移动到位置 0Vue 通过 key 识别出这是同一个元素,直接移动 DOM 节点,保持了 checkbox 的状态。
key 的工作原理:
Vue 的 Diff 算法使用 key 来建立新旧节点的映射关系:
1// 简化的 Diff 算法2function updateChildren(oldChildren, newChildren) {3 // 1. 使用 key 建立旧节点的映射表4 const oldKeyToIdx = {}5 oldChildren.forEach((child, idx) => {6 if (child.key) {7 oldKeyToIdx[child.key] = idx8 }9 })10 11 // 2. 遍历新节点列表12 newChildren.forEach(newChild => {13 if (newChild.key && oldKeyToIdx[newChild.key] !== undefined) {14 // 找到相同 key 的旧节点,复用并更新15 const oldIdx = oldKeyToIdx[newChild.key]16 const oldChild = oldChildren[oldIdx]17 patch(oldChild, newChild) // 复用 DOM,只更新差异18 } else {19 // 没找到,创建新节点20 createNode(newChild)21 }22 })23 24 // 3. 删除不再需要的旧节点25 // ...26}有 key 的优势:
- ⚡ O(1) 时间复杂度查找对应节点
- 🎯 精确复用 DOM 元素
- 🔄 正确保持组件状态
key 的选择原则:
- ✅ 推荐的 key
- ❌ 不推荐的 key
1. 使用唯一 ID(最佳)
1<template>2 <!-- ✅ 使用数据的唯一标识 -->3 <div v-for="item in items" :key="item.id">4 {{ item.name }}5 </div>6 7 <!-- ✅ 使用唯一字符串 -->8 <div v-for="user in users" :key="user.email">9 {{ user.name }}10 </div>11 12 <!-- ✅ 使用 UUID -->13 <div v-for="item in items" :key="item.uuid">14 {{ item.title }}15 </div>16</template>1718<script>19export default {20 data() {21 return {22 items: [23 { id: 1, name: 'Item 1' },24 { id: 2, name: 'Item 2' },25 { id: 3, name: 'Item 3' }26 ],27 users: [28 { email: 'alice@example.com', name: 'Alice' },29 { email: 'bob@example.com', name: 'Bob' }30 ]31 }32 }33}34</script>2. 组合多个字段(确保唯一)
1<template>2 <!-- ✅ 组合字段确保唯一性 -->3 <div v-for="item in items" :key="`${item.type}-${item.id}`">4 {{ item.name }}5 </div>6 7 <!-- ✅ 使用对象的多个属性 -->8 <div v-for="order in orders" :key="`order-${order.userId}-${order.orderId}`">9 {{ order.productName }}10 </div>11</template>3. 静态列表可以使用索引
1<template>2 <!-- ✅ 静态列表(不会重排、增删)可以使用索引 -->3 <div v-for="(color, index) in staticColors" :key="index">4 {{ color }}5 </div>6</template>78<script>9export default {10 data() {11 return {12 // 这个列表永远不会改变顺序或增删13 staticColors: ['red', 'green', 'blue']14 }15 }16}17</script>1. 不要使用索引作为 key(动态列表)
1<template>2 <!-- ❌ 错误:动态列表使用索引 -->3 <div v-for="(item, index) in items" :key="index">4 <input type="checkbox" v-model="item.checked" />5 {{ item.name }}6 </div>7</template>89<script>10export default {11 data() {12 return {13 items: [14 { id: 1, name: 'A', checked: false },15 { id: 2, name: 'B', checked: false },16 { id: 3, name: 'C', checked: false }17 ]18 }19 },20 methods: {21 deleteFirst() {22 this.items.shift() // 删除第一项23 }24 }25}26</script>问题分析:
1// 初始数据2items = [3 { id: 1, name: 'A' }, // index: 0, key: 04 { id: 2, name: 'B' }, // index: 1, key: 15 { id: 3, name: 'C' } // index: 2, key: 26]78// 删除第一项后9items = [10 { id: 2, name: 'B' }, // index: 0, key: 0 (之前是 key: 1)11 { id: 3, name: 'C' } // index: 1, key: 1 (之前是 key: 2)12]1314// 问题:15// - Vue 认为 key=0 的元素还是原来的元素(实际上是 B)16// - Vue 认为 key=1 的元素还是原来的元素(实际上是 C)17// - Vue 认为 key=2 的元素被删除了18// 结果:Vue 会错误地复用 DOM,导致状态混乱2. 不要使用随机数
1<template>2 <!-- ❌ 错误:使用随机数 -->3 <div v-for="item in items" :key="Math.random()">4 {{ item.name }}5 </div>6</template>问题:
- 每次渲染都会生成新的 key
- Vue 无法识别哪些元素是相同的
- 导致所有元素都被重新创建,性能极差
3. 不要使用非原始类型
1<template>2 <!-- ❌ 错误:使用对象作为 key -->3 <div v-for="item in items" :key="item">4 {{ item.name }}5 </div>6</template>问题:
- 对象的引用每次都不同
- Vue 无法正确比较 key
- 导致无法复用 DOM
性能对比:
测试场景:1000 个元素的列表反转
| 方式 | 操作 | 耗时 | 说明 |
|---|---|---|---|
| 无 key | 反转列表 | ~200ms | 更新所有元素的内容 |
| 有 key (ID) | 反转列表 | ~50ms | 只移动 DOM 节点位置 |
| 错误 key (index) | 删除第一项 | ~150ms | 错误复用,更新大量元素 |
| 错误 key (random) | 任何操作 | ~500ms | 重新创建所有元素 |
结论:
- 使用正确的 key 可以提升 4 倍性能
- 使用错误的 key 比不用 key 更差
实际案例:
- 待办事项列表
- 可拖拽列表
- 动态表单
1<template>2 <div class="todo-list">3 <h2>待办事项</h2>4 5 <!-- ✅ 使用唯一 ID 作为 key -->6 <div 7 v-for="todo in todos" 8 :key="todo.id"9 class="todo-item"10 >11 <input 12 type="checkbox" 13 v-model="todo.completed"14 :id="`todo-${todo.id}`"15 />16 <label :for="`todo-${todo.id}`">17 {{ todo.text }}18 </label>19 <button @click="deleteTodo(todo.id)">删除</button>20 <button @click="moveTodoUp(todo.id)">上移</button>21 </div>22 23 <button @click="addTodo">添加待办</button>24 </div>25</template>2627<script>28export default {29 data() {30 return {31 todos: [32 { id: 1, text: '学习 Vue', completed: false },33 { id: 2, text: '写代码', completed: true },34 { id: 3, text: '看文档', completed: false }35 ],36 nextId: 437 }38 },39 methods: {40 addTodo() {41 this.todos.push({42 id: this.nextId++,43 text: `新待办 ${this.nextId}`,44 completed: false45 })46 },47 deleteTodo(id) {48 const index = this.todos.findIndex(t => t.id === id)49 this.todos.splice(index, 1)50 },51 moveTodoUp(id) {52 const index = this.todos.findIndex(t => t.id === id)53 if (index > 0) {54 const todo = this.todos.splice(index, 1)[0]55 this.todos.splice(index - 1, 0, todo)56 }57 }58 }59}60</script>为什么需要 key:
- ✅ 删除待办时,其他待办的 checkbox 状态不会错乱
- ✅ 上移待办时,checkbox 状态正确跟随
- ✅ 添加新待办时,不会影响现有待办的状态
1<template>2 <div class="draggable-list">3 <h2>可拖拽排序列表</h2>4 5 <!-- ✅ 拖拽列表必须使用 key -->6 <draggable 7 v-model="items" 8 @start="drag=true" 9 @end="drag=false"10 >11 <transition-group type="transition" name="flip-list">12 <div 13 v-for="item in items" 14 :key="item.id"15 class="list-item"16 >17 <span class="handle">☰</span>18 <span>{{ item.name }}</span>19 <input v-model="item.value" placeholder="输入值" />20 </div>21 </transition-group>22 </draggable>23 </div>24</template>2526<script>27import draggable from 'vuedraggable'2829export default {30 components: {31 draggable32 },33 data() {34 return {35 drag: false,36 items: [37 { id: 1, name: '项目 1', value: '' },38 { id: 2, name: '项目 2', value: '' },39 { id: 3, name: '项目 3', value: '' }40 ]41 }42 }43}44</script>4546<style scoped>47.flip-list-move {48 transition: transform 0.5s;49}50</style>为什么需要 key:
- ✅ 拖拽时,input 的输入值正确跟随元素移动
- ✅ 过渡动画正确触发
- ✅ 组件状态不会混乱
1<template>2 <div class="dynamic-form">3 <h2>动态表单</h2>4 5 <!-- ✅ 动态表单字段必须使用 key -->6 <div 7 v-for="field in formFields" 8 :key="field.id"9 class="form-field"10 >11 <label>{{ field.label }}</label>12 <input 13 :type="field.type"14 v-model="field.value"15 :placeholder="field.placeholder"16 />17 <button @click="removeField(field.id)">删除</button>18 </div>19 20 <button @click="addField">添加字段</button>21 </div>22</template>2324<script>25export default {26 data() {27 return {28 formFields: [29 { 30 id: 1, 31 type: 'text', 32 label: '姓名', 33 value: '',34 placeholder: '请输入姓名'35 },36 { 37 id: 2, 38 type: 'email', 39 label: '邮箱', 40 value: '',41 placeholder: '请输入邮箱'42 }43 ],44 nextId: 345 }46 },47 methods: {48 addField() {49 this.formFields.push({50 id: this.nextId++,51 type: 'text',52 label: `字段 ${this.nextId}`,53 value: '',54 placeholder: '请输入内容'55 })56 },57 removeField(id) {58 const index = this.formFields.findIndex(f => f.id === id)59 this.formFields.splice(index, 1)60 }61 }62}63</script>为什么需要 key:
- ✅ 删除字段时,其他字段的输入值不会错乱
- ✅ 添加字段时,不会影响现有字段的状态
- ✅ 字段顺序改变时,输入值正确保持
错误 1:在动态列表中使用索引
1<!-- ❌ 错误 -->2<div v-for="(item, index) in items" :key="index">3 <input v-model="item.value" />4</div>5<!-- 问题:删除、插入、排序时会导致状态混乱 -->67<!-- ✅ 正确 -->8<div v-for="item in items" :key="item.id">9 <input v-model="item.value" />10</div>错误 2:不使用 key
1<!-- ❌ 错误 -->2<div v-for="item in items">3 {{ item.name }}4</div>5<!-- 问题:Vue 会警告,且可能导致状态问题 -->67<!-- ✅ 正确 -->8<div v-for="item in items" :key="item.id">9 {{ item.name }}10</div>错误 3:key 不唯一
1<!-- ❌ 错误 -->2<div v-for="item in items" :key="item.type">3 {{ item.name }}4</div>5<!-- 问题:多个元素可能有相同的 type,导致 key 重复 -->67<!-- ✅ 正确 -->8<div v-for="item in items" :key="item.id">9 {{ item.name }}10</div>总结:
key 的作用:
- 🎯 帮助 Vue 识别节点的身份
- ⚡ 提高 Diff 算法的效率
- 🔄 避免就地复用导致的问题
- 🎨 保持组件状态的正确性
- 🎭 正确触发过渡动画
使用原则:
- ✅ 必须使用唯一且稳定的值
- ✅ 优先使用数据的唯一标识(如 ID)
- ❌ 不要使用索引(除非列表是静态的)
- ❌ 不要使用随机数
- ❌ 不要使用非原始类型
记忆口诀: "v-for 必加 key,唯一稳定是关键,索引随机要避免,ID 最佳保平安"
在实际项目中,我总是为 v-for 添加 key,这是一个良好的编程习惯,可以避免很多潜在的问题。
12.3 v-for 和 v-if 能一起使用吗
面试回答思路:
这是一个非常常见的面试题,也是实际开发中容易犯的错误。需要理解 Vue 2 和 Vue 3 的优先级差异,以及正确的解决方案。
不推荐在同一个元素上同时使用 v-for 和 v-if!
- ⚠️ Vue 2:v-for 优先级高,会遍历所有元素再判断,性能浪费
- ⚠️ Vue 3:v-if 优先级高,可能访问不到 v-for 的变量,导致报错
- ✅ 推荐:使用计算属性过滤数据,或使用 template 标签分离
Vue 2 vs Vue 3 优先级对比:
- Vue 2 的问题
- Vue 3 的问题
Vue 2:v-for 优先级高于 v-if
1<template>2 <ul>3 <!-- ❌ 不推荐:Vue 2 -->4 <li v-for="user in users" v-if="user.isActive" :key="user.id">5 {{ user.name }}6 </li>7 </ul>8</template>910<script>11export default {12 data() {13 return {14 users: [15 { id: 1, name: 'Alice', isActive: true },16 { id: 2, name: 'Bob', isActive: false },17 { id: 3, name: 'Charlie', isActive: true },18 // ... 假设有 1000 个用户19 ]20 }21 }22}23</script>问题分析:
1// Vue 2 的执行顺序2for (let user of users) { // 1. 先执行 v-for,遍历所有 1000 个用户3 if (user.isActive) { // 2. 再执行 v-if,判断每个用户4 render(user) // 3. 渲染符合条件的用户5 }6}78// 性能问题:9// - 即使只有 100 个活跃用户,也要遍历所有 1000 个用户10// - 每次渲染都要执行 1000 次判断11// - 浪费了 900 次无效的判断性能影响:
| 场景 | 总用户数 | 活跃用户数 | 遍历次数 | 判断次数 | 渲染次数 |
|---|---|---|---|---|---|
| 小型应用 | 100 | 50 | 100 | 100 | 50 |
| 中型应用 | 1000 | 100 | 1000 | 1000 | 100 |
| 大型应用 | 10000 | 500 | 10000 | 10000 | 500 |
每次组件更新都要执行这么多次操作,性能开销巨大!
Vue 3:v-if 优先级高于 v-for
1<template>2 <ul>3 <!-- ❌ 错误:Vue 3 -->4 <li v-if="user.isActive" v-for="user in users" :key="user.id">5 {{ user.name }}6 </li>7 </ul>8</template>问题分析:
1// Vue 3 的执行顺序2if (user.isActive) { // 1. 先执行 v-if3 for (let user of users) { // 2. 再执行 v-for4 render(user)5 }6}78// 错误原因:9// - v-if 先执行,但此时 user 变量还不存在10// - 会报错:ReferenceError: user is not defined控制台错误:
1[Vue warn]: Property "user" was accessed during render but is not defined on instance.2ReferenceError: user is not defined正确的解决方案:
- 方案 1:计算属性(推荐)
- 方案 2:template 标签
- 方案 3:外层 v-if
使用计算属性过滤数据(最佳实践)
1<template>2 <ul>3 <!-- ✅ 推荐:使用计算属性 -->4 <li v-for="user in activeUsers" :key="user.id">5 {{ user.name }}6 </li>7 </ul>8</template>910<script>11export default {12 data() {13 return {14 users: [15 { id: 1, name: 'Alice', isActive: true },16 { id: 2, name: 'Bob', isActive: false },17 { id: 3, name: 'Charlie', isActive: true },18 { id: 4, name: 'David', isActive: false },19 { id: 5, name: 'Eve', isActive: true }20 ]21 }22 },23 computed: {24 activeUsers() {25 // 只过滤一次,返回活跃用户26 return this.users.filter(user => user.isActive)27 }28 }29}30</script>优点:
- ✅ 只遍历需要显示的元素(100 次而不是 1000 次)
- ✅ 有缓存,依赖不变时不重新计算
- ✅ 代码清晰易读,逻辑分离
- ✅ 性能最好
性能对比:
1// ❌ 同时使用 v-for 和 v-if2// 每次渲染:1000 次遍历 + 1000 次判断 = 2000 次操作34// ✅ 使用计算属性5// 首次渲染:1000 次过滤 + 100 次遍历 = 1100 次操作6// 后续渲染:100 次遍历(有缓存)7// 性能提升:约 10 倍使用 template 标签分离 v-if 和 v-for
1<template>2 <ul>3 <!-- ✅ 可以:使用 template 标签 -->4 <template v-for="user in users" :key="user.id">5 <li v-if="user.isActive">6 {{ user.name }}7 </li>8 </template>9 </ul>10</template>1112<script>13export default {14 data() {15 return {16 users: [17 { id: 1, name: 'Alice', isActive: true },18 { id: 2, name: 'Bob', isActive: false },19 { id: 3, name: 'Charlie', isActive: true }20 ]21 }22 }23}24</script>优点:
- ✅ 不会产生额外的 DOM 元素
- ✅ 逻辑清晰,v-if 和 v-for 分离
- ✅ 兼容 Vue 2 和 Vue 3
缺点:
- ❌ 仍然会遍历所有元素(1000 次)
- ❌ 性能不如计算属性
- ❌ 代码稍显冗余
适用场景:
- 过滤逻辑复杂,不适合放在计算属性中
- 需要在循环中使用多个条件判断
在外层使用 v-if 控制整个列表
1<template>2 <div>3 <!-- ✅ 可以:控制整个列表的显示 -->4 <ul v-if="users.length > 0">5 <li v-for="user in users" :key="user.id">6 {{ user.name }}7 </li>8 </ul>9 <p v-else>暂无用户</p>10 11 <!-- ✅ 可以:根据条件显示不同列表 -->12 <div v-if="showActiveUsers">13 <h3>活跃用户</h3>14 <ul>15 <li v-for="user in activeUsers" :key="user.id">16 {{ user.name }}17 </li>18 </ul>19 </div>20 </div>21</template>2223<script>24export default {25 data() {26 return {27 users: [],28 showActiveUsers: true29 }30 },31 computed: {32 activeUsers() {33 return this.users.filter(user => user.isActive)34 }35 }36}37</script>适用场景:
- 控制整个列表的显示隐藏
- 根据条件渲染不同的列表
- 不是过滤列表项,而是控制列表本身
实际案例对比:
- 商品列表过滤
- 嵌套列表
- 条件渲染列表
1<template>2 <div class="product-list">3 <h2>商品列表</h2>4 5 <!-- ❌ 错误做法 -->6 <div class="bad-example">7 <h3>❌ 不推荐(同时使用)</h3>8 <div 9 v-for="product in products" 10 v-if="product.inStock" 11 :key="product.id"12 class="product-card"13 >14 {{ product.name }} - ¥{{ product.price }}15 </div>16 </div>17 18 <!-- ✅ 正确做法 -->19 <div class="good-example">20 <h3>✅ 推荐(计算属性)</h3>21 <div 22 v-for="product in inStockProducts" 23 :key="product.id"24 class="product-card"25 >26 {{ product.name }} - ¥{{ product.price }}27 </div>28 </div>29 </div>30</template>3132<script>33export default {34 data() {35 return {36 products: [37 { id: 1, name: '商品1', price: 99, inStock: true },38 { id: 2, name: '商品2', price: 199, inStock: false },39 { id: 3, name: '商品3', price: 299, inStock: true },40 { id: 4, name: '商品4', price: 399, inStock: false },41 { id: 5, name: '商品5', price: 499, inStock: true }42 ]43 }44 },45 computed: {46 inStockProducts() {47 return this.products.filter(p => p.inStock)48 }49 }50}51</script>1<template>2 <div class="category-list">3 <h2>商品分类</h2>4 5 <!-- ✅ 正确:外层 v-for,内层 v-if -->6 <div v-for="category in categories" :key="category.id" class="category">7 <h3>{{ category.name }}</h3>8 9 <!-- 方式 1:使用计算属性 -->10 <ul v-if="getCategoryProducts(category.id).length > 0">11 <li v-for="product in getCategoryProducts(category.id)" :key="product.id">12 {{ product.name }}13 </li>14 </ul>15 <p v-else>该分类暂无商品</p>16 17 <!-- 方式 2:使用 template -->18 <ul>19 <template v-for="product in category.products" :key="product.id">20 <li v-if="product.visible">21 {{ product.name }}22 </li>23 </template>24 </ul>25 </div>26 </div>27</template>2829<script>30export default {31 data() {32 return {33 categories: [34 {35 id: 1,36 name: '电子产品',37 products: [38 { id: 1, name: '手机', visible: true },39 { id: 2, name: '电脑', visible: false },40 { id: 3, name: '平板', visible: true }41 ]42 },43 {44 id: 2,45 name: '图书',46 products: [47 { id: 4, name: '小说', visible: true },48 { id: 5, name: '教材', visible: true }49 ]50 }51 ]52 }53 },54 methods: {55 getCategoryProducts(categoryId) {56 const category = this.categories.find(c => c.id === categoryId)57 return category ? category.products.filter(p => p.visible) : []58 }59 }60}61</script>1<template>2 <div class="user-dashboard">3 <h2>用户面板</h2>4 5 <!-- ✅ 正确:外层 v-if 控制整个列表 -->6 <div v-if="isLoggedIn">7 <h3>我的订单</h3>8 <ul v-if="orders.length > 0">9 <li v-for="order in orders" :key="order.id">10 订单号:{{ order.id }} - 状态:{{ order.status }}11 </li>12 </ul>13 <p v-else>暂无订单</p>14 </div>15 <div v-else>16 <p>请先登录</p>17 </div>18 19 <!-- ✅ 正确:使用计算属性过滤 -->20 <div>21 <h3>待处理订单</h3>22 <ul>23 <li v-for="order in pendingOrders" :key="order.id">24 {{ order.id }}25 </li>26 </ul>27 </div>28 </div>29</template>3031<script>32export default {33 data() {34 return {35 isLoggedIn: true,36 orders: [37 { id: 1, status: 'pending' },38 { id: 2, status: 'completed' },39 { id: 3, status: 'pending' }40 ]41 }42 },43 computed: {44 pendingOrders() {45 return this.orders.filter(order => order.status === 'pending')46 }47 }48}49</script>ESLint 规则:
Vue 官方的 ESLint 插件会检测这个问题:
1// .eslintrc.js2module.exports = {3 extends: [4 'plugin:vue/vue3-recommended'5 ],6 rules: {7 // 禁止 v-if 和 v-for 在同一元素上使用8 'vue/no-use-v-if-with-v-for': 'error'9 }10}错误提示:
1error: The 'v-if' directive should not be used on the same element as 'v-for' (vue/no-use-v-if-with-v-for)性能对比总结:
| 方案 | 遍历次数 | 判断次数 | 缓存 | 性能评分 | 推荐度 |
|---|---|---|---|---|---|
| 同时使用(Vue 2) | 1000 | 1000 | ❌ | ⭐ | ❌ |
| 同时使用(Vue 3) | - | - | ❌ | - | ❌ 报错 |
| 计算属性 | 100 | 1000(一次) | ✅ | ⭐⭐⭐⭐⭐ | ✅✅✅ |
| template 标签 | 1000 | 1000 | ❌ | ⭐⭐⭐ | ✅ |
| 外层 v-if | 取决于条件 | 1 | ❌ | ⭐⭐⭐⭐ | ✅✅ |
记忆口诀: "v-for 和 v-if 不同居,计算属性来过滤"
选择指南:
- 优先使用计算属性:性能最好,代码最清晰
- template 标签:逻辑复杂时使用
- 外层 v-if:控制整个列表显示时使用
- 绝不同时使用:无论 Vue 2 还是 Vue 3
核心原则:
- 🎯 数据过滤在 JavaScript 中完成(计算属性)
- 🎨 模板只负责渲染,不负责过滤
- ⚡ 减少不必要的遍历和判断
- 📝 保持代码清晰易维护
在实际项目中,我总是使用计算属性来过滤列表数据,这样不仅性能好,代码也更清晰易维护。这是 Vue 开发中的重要最佳实践。
十三、总结
这份 Vue 面试题库涵盖了:
- ✅ Vue 基础概念和核心特性
- ✅ 组件通信的多种方式
- ✅ 生命周期钩子详解
- ✅ 指令系统和自定义指令
- ✅ Computed 和 Watch 的使用
- ✅ 虚拟 DOM 和 Diff 算法
- ✅ Vue Router 路由管理
- ✅ Vuex/Pinia 状态管理
- ✅ 性能优化技巧
- ✅ 常见问题和最佳实践
- ✅ 前后台交互(Fetch、Axios、async/await)
- ✅ 常见面试题补充(v-if vs v-show、v-for key、v-for 与 v-if)
建议结合实际项目经验,深入理解每个知识点的应用场景。
评论区 / Comments