前端面试之 Vue 50题及参考答案
Vue3 作为现代前端框架的佼佼者,其新特性和性能优化是面试中的考察重点。本文将深入剖析 50 道高频 Vue3 面试题,助你全面掌握核心概念。
一、基础概念与新特性
1. Vue3 相比 Vue2 有哪些主要改进?
Vue3 带来了多项重大革新:
- 响应式系统:使用
Proxy
替代Object.defineProperty
,能监听对象属性的添加、删除及数组索引变化,解决了 Vue2 的响应式局限性。 - 性能提升:包括重构的虚拟 DOM (引入 PatchFlag、静态提升)、事件缓存等,渲染速度更快。
- Tree-shaking:模块化架构支持,未使用的代码不会被打包,减小应用体积。
- Composition API:提供更灵活的逻辑组织和复用方式,替代 Options API。
- TypeScript 支持:源码使用 TypeScript 重写,提供更好的类型推断。
- 新组件:引入
Teleport
、Suspense
、Fragment
等内置组件。 - 自定义渲染器:支持更灵活的自定义渲染器 API。
2. 什么是 Composition API?它的作用是什么?
Composition API 是 Vue3 引入的一套新的 API,允许开发者使用导入的函数来组织组件逻辑,而不是依赖选项(如 data
, methods
, computed
)。它的核心是 setup
函数。
作用:
- 更好的逻辑组织:将相关联的逻辑代码组织在一起,而不是按选项分散。
- 逻辑复用:通过自定义组合式函数,可以轻松地在不同组件间复用逻辑,解决了 Vue2 Mixins 的命名冲突和来源不清晰问题。
- 更好的 TypeScript 支持:与 TypeScript 的类型推断系统配合得更好。
示例:一个简单的计数器逻辑复用
// useCounter.js - 一个组合式函数
import { ref } from 'vue';
export function useCounter(initialValue = 0) {
const count = ref(initialValue);
const increment = () => {
count.value++;
};
const decrement = () => {
count.value--;
};
return {
count,
increment,
decrement
};
}
// MyComponent.vue - 在组件中使用
import { useCounter } from './useCounter';
export default {
setup() {
const { count, increment, decrement } = useCounter();
return {
count,
increment,
decrement
};
}
};
3. Vue3 的生命周期钩子有哪些变化?
Vue3 的生命周期钩子与 Vue2 类似,但有一些重要变化和新增钩子:
Vue2 选项 | Vue3 组合式 API Hook | 说明 |
---|---|---|
beforeCreate | - | 被 setup() 替代 |
created | - | 被 setup() 替代 |
beforeMount | onBeforeMount | 组件挂载前调用 |
mounted | onMounted | 组件挂载后调用 |
beforeUpdate | onBeforeUpdate | 组件更新前调用 |
updated | onUpdated | 组件更新后调用 |
beforeDestroy | onBeforeUnmount | 名称变更,组件卸载前调用 |
destroyed | onUnmounted | 名称变更,组件卸载后调用 |
activated | onActivated | 被 <keep-alive> 缓存的组件激活时调用 |
deactivated | onDeactivated | 被 <keep-alive> 缓存的组件失活时调用 |
errorCaptured | onErrorCaptured | 捕获来自子孙组件的错误时调用 |
- | onRenderTracked | 新增 (开发模式),当响应式依赖被追踪时调用 (调试用) |
- | onRenderTriggered | 新增 (开发模式),当响应式依赖触发组件重新渲染时调用 (调试用) |
主要变化:
beforeDestroy
和destroyed
被重命名为onBeforeUnmount
和onUnmounte
d,更准确地描述了其功能。setup()
函数在beforeCreate
和created
生命周期之间运行,意味着在这两个钩子中编写的代码都应直接在setup()
中编写。- 所有生命周期钩子都需要从
vue
中导入并在setup()
中使用。 - 可以在
setup()
中多次注册同一个生命周期钩子,它们会按照注册顺序依次执行。
示例:在 setup
中使用生命周期钩子
import { onMounted, onUnmounted } from 'vue';
export default {
setup() {
onMounted(() => {
console.log('Component is mounted!');
// 执行一些初始化操作,例如添加事件监听器
});
onUnmounted(() => {
console.log('Component is unmounted!');
// 执行清理操作,例如移除事件监听器
});
}
};
4. 什么是 setup
函数?
setup
函数是 Composition API 的入口点,在组件创建之前执行(在 beforeCreate
和 created
生命周期钩子之前),此时组件实例尚未生成,因此 this
不可用。
参数:
props
: 组件接收的 props,是响应式的。context
: 一个普通 JavaScript 对象,暴露了在组件中可能常用的三个属性:attrs
: 包含未在 props 中声明的 attribute。slots
: 包含所有传入的插槽内容。emit
: 用于触发事件的方法。
返回值: setup
返回一个对象,该对象的所有属性都会暴露给组件的模板和其他的选项(如 methods
)使用。
作用:
- 定义响应式数据、计算属性、方法等。
- 注册生命周期钩子。
- 返回模板需要使用的数据和方法。
示例:基本的 setup
用法
import { ref } from 'vue';
export default {
props: {
title: String
},
setup(props, context) {
const count = ref(0);
const increment = () => {
count.value++;
};
// 使用 context
context.emit('customEvent');
// 返回模板可用的内容
return {
count,
increment
};
}
};
5. Vue3 中的响应式系统是如何工作的?
Vue3 使用 JavaScript 的 Proxy
对象来实现响应式,这相较于 Vue2 的 Object.defineProperty
是一次重大的升级。
工作原理简述:
- ** reactive 函数**: 当你使用
reactive()
包裹一个对象时,Vue 会返回该对象的 Proxy 代理。这个 Proxy 可以拦截(intercept)对目标对象的各种操作,例如属性读取 (get
)、属性设置 (set
)、属性删除 (deleteProperty
) 等。 - 依赖追踪 (Track): 当在副作用函数(例如组件的渲染函数或
watchEffect
)中读取 (get
) 一个响应式对象的属性时,Vue 会追踪这个属性与当前正在运行的副作用函数的依赖关系,并将其存储起来。 - 触发更新 (Trigger): 当你修改 (
set
) 或删除 (deleteProperty
) 一个响应式对象的属性时,Proxy 会拦截这个操作。Vue 会找到所有依赖于这个属性的副作用函数,并重新执行它们,从而触发组件的重新渲染或watchEffect
的重新执行。
Proxy 的优势:
- 可检测新增/删除属性: 无需使用
Vue.set
/Vue.delete
。 - 可检测数组索引变化及
.length
修改。 - 性能更好: 按需拦截,惰性创建子对象的 Proxy(只有在访问深层属性时才会递归创建)。
- 支持更多数据结构: 如
Map
,Set
,WeakMap
,WeakSet
。
简易代码模拟:
// 极简版的 reactive 实现原理
function reactive(target) {
return new Proxy(target, {
get(obj, key) {
track(obj, key); // 依赖追踪:记录“谁读取了这个对象的这个属性”
return Reflect.get(obj, key);
},
set(obj, key, value) {
const result = Reflect.set(obj, key, value);
trigger(obj, key); // 触发更新:通知所有依赖于此属性的地方“值变了,快更新!”
return result;
},
deleteProperty(obj, key) {
const result = Reflect.deleteProperty(obj, key);
trigger(obj, key); // 触发更新
return result;
}
});
}
// 存储依赖关系的桶(实际Vue的实现更复杂)
const targetMap = new WeakMap(); // 用于存储每个目标对象及其键的依赖
function track(target, key) {
// ... 收集当前正在运行的副作用函数到 targetMap[target][key] 中
}
function trigger(target, key) {
// ... 从 targetMap[target][key] 中取出所有副作用函数并执行
}
二、Composition API 核心概念
6. ref
和 reactive
的区别是什么?它们的使用场景是怎样的?
ref
和 reactive
是 Vue3 创建响应式数据的两个核心函数,它们有不同的用途。
特性 | ref | reactive |
---|---|---|
数据类型 | 主要用于基本类型(字符串、数字、布尔值),也可用于对象和数组 | 主要用于对象或数组等引用类型 |
返回值 | 返回一个响应式对象(RefImpl),其值在 .value 属性中 | 返回原始对象的 Proxy 代理对象 |
访问方式 | 在 JavaScript 中需要通过 .value 访问;在模板中自动解包,无需 .value | 直接访问属性即可 |
解构/扩展 | 使用 toRef 或 toRefs 解构才能保持响应性 | 直接解构或扩展会丢失响应性,需使用 toRefs |
模板引用 | 可用于模板 ref,获取 DOM 元素或组件实例 | 不能用于模板 ref |
使用场景:
ref
: 当你有一个独立的基本类型值需要变为响应式时(如count
,inputValue
),或者在逻辑上它是一个需要被“引用”的单一值。也常用于模板 ref。const count = ref(0); const inputEl = ref(null); // 模板ref
reactive
: 当你有一组** logically related**(逻辑上相关联)的数据,并且它们通常以对象的形式组织在一起时(如表单数据、模块状态)。const formState = reactive({ username: '', password: '', rememberMe: false });
7. computed
和 watch
的区别是什么?
computed
和 watch
都用于响应数据变化,但目的和用法不同。
特性 | computed | watch / watchEffect |
---|---|---|
目的 | 声明一个依赖其他响应式数据的计算属性,其值基于依赖缓存 | 执行副作用操作,如监听数据变化后发起异步请求、操作DOM等 |
返回值 | 返回一个 ref 对象,代表计算后的值 | 无返回值 |
缓存 | 有缓存,只有依赖变化时才重新计算 | 无缓存,只要监听的值变化就执行 |
异步 | 不能包含异步操作 | 可以包含异步操作 |
立即执行 | 默认不会立即执行,依赖变化才执行 | watch 可配置 immediate: true 来立即执行;watchEffect 会立即执行 |
computed
示例:
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
// 计算属性 fullName
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`;
});
// 在模板中使用:{{ fullName }} (无需 .value)
watch
示例:
import { ref, watch } from 'vue';
const userId = ref(1);
const userData = ref(null);
// 监听 userId 的变化,并异步获取用户数据
watch(userId, async (newId, oldId) => {
const response = await fetch(`/api/users/${newId}`);
userData.value = await response.json();
}, { immediate: true }); // 立即执行一次,在组件创建时获取初始用户数据
watchEffect
示例:
import { ref, watchEffect } from 'vue';
const count = ref(0);
// watchEffect 会自动追踪其内部使用的所有响应式依赖
watchEffect(() => {
console.log(`Count is now: ${count.value}`); // 会自动追踪 count.value
// 当 count.value 变化时,这个函数会重新执行
});
8. 什么是 watchEffect
?它与 watch
有何不同?
watchEffect
是 Vue3 引入的一个新的 API,用于立即运行一个副作用函数,并自动追踪该函数内部所使用的所有响应式依赖,并在这些依赖发生更改时重新运行该函数。
与 watch
的主要区别:
特性 | watch | watchEffect |
---|---|---|
依赖源指定方式 | 显式指定一个或多个要监听的响应式引用或 getter 函数 | 自动收集副作用函数中使用的所有响应式属性 |
旧值 | 在回调函数中可以获取旧值和新值 | 无法获取旧值 |
立即执行 | 默认不立即执行,可配置 immediate: true 来实现 | 立即执行,然后在依赖变更时重新执行 |
适用场景 | 适用于需要明确知道是哪个依赖发生了变化,或者需要对比旧值和新值的场景 | 适用于依赖关系不那么明确,或者多个依赖中任何一个变化都需要执行副作用的场景(如日志记录、请求取消) |
示例对比:
// 使用 watch:需要明确指定要监听的源
const firstName = ref('');
const lastName = ref('');
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`Name changed from ${oldFirst} ${oldLast} to ${newFirst} ${newLast}`);
});
// 使用 watchEffect:自动追踪 firstName 和 lastName
watchEffect(() => {
console.log(`Full name is: ${firstName.value} ${lastName.value}`);
// 这个函数会因 firstName.value 或 lastName.value 的变化而重新执行
});
9. 如何在 setup
中访问组件实例?
在 setup
中,由于执行时机过早,this
不再是组件实例的引用。要访问组件实例,可以使用 getCurrentInstance
函数。
import { getCurrentInstance } from 'vue';
export default {
setup() {
const instance = getCurrentInstance(); // 获取当前组件实例
console.log(instance); // 包含 proxy, ctx, parent 等属性
// 通过 instance.proxy 可以访问组件上下文,类似于 this
// 例如:instance.proxy.$route, instance.proxy.$router (如果使用了Vue Router)
// 注意:getCurrentInstance 主要用于高级用例或库的开发,在绝大多数应用代码中应避免使用,
// 因为它会使代码更难以理解和测试,且可能不兼容 SSR。
}
};
注意: getCurrentInstance
仅在开发或调试中有特定用途,在业务逻辑中应尽量避免使用,以保持组合函数的纯粹性和可复用性。通常,需要的功能(如访问 root
或 parent
)可以通过 provide
/inject
或 props 来实现。
10. 什么是 toRef
和 toRefs
?它们有什么用?
toRef
和 toRefs
是用于保持响应式对象属性响应性的工具函数,在处理组合式函数返回或解构 reactive
对象时非常有用。
toRef
: 为响应式对象 (reactive
创建) 上的某个属性创建一个 ref。这个 ref 是响应式地链接到原对象的该属性上,修改 ref 的值会更新原对象,反之亦然。import { reactive, toRef } from 'vue'; const state = reactive({ foo: 1, bar: 2 }); const fooRef = toRef(state, 'foo'); // 创建一个链接到 state.foo 的 ref fooRef.value++; // state.foo 现在是 2 console.log(state.foo); // 2 state.foo++; // fooRef.value 现在是 3 console.log(fooRef.value); // 3
toRefs
: 将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向原对象相应属性的 ref。这在从组合式函数返回响应式对象或在解构时保持响应性非常有用。import { reactive, toRefs } from 'vue'; // 在组合式函数中 function useFeatureX() { const state = reactive({ x: 0, y: 0 }); // ... 逻辑操作 state return toRefs(state); // 返回时转换为 refs,这样使用方可以解构而不会失去响应性 } // 在组件中使用 export default { setup() { // 可以解构,同时保持响应性! const { x, y } = useFeatureX(); return { x, y }; } };
toRefs
在解构 reactive
对象时的应用:
import { reactive, toRefs } from 'vue';
export default {
setup() {
const state = reactive({
count: 0,
name: 'Vue'
});
// 直接解构会失去响应性!
// let { count, name } = state; // ❌ 非响应式
// 使用 toRefs 解构保持响应性
let { count, name } = toRefs(state); // ✅ 现在是两个 ref
// 在 JavaScript 中访问需要 .value
console.log(count.value);
// 在模板中可以直接使用,会自动解包
return {
count,
name
};
}
};
三、组件通信
11. Vue3 中组件通信的方式有哪些?
Vue3 提供了多种组件通信方式,适用于不同场景:
- Props / Emits:最基础的父子组件通信方式。
- v-model / .sync:语法糖,实现父子组件数据的双向绑定。
- Refs:父组件通过 ref 获取子组件实例,调用其方法或访问其数据。
- Provide / Inject:跨层级组件通信,祖先组件提供数据,后代组件注入使用。
- Event Bus:利用第三方库(如
mitt
)实现全局事件监听和触发,适用于任意组件间通信。 - Vuex / Pinia:全局状态管理库,集中管理所有组件共享的状态。
- 插槽 (Slots):父组件向子组件传递模板内容。
- 默认插槽
- 具名插槽
- 作用域插槽:子组件向父组件的插槽内容传递数据。
12. provide
和 inject
的作用是什么?如何使用?
provide
和 inject
用于实现跨层级组件通信,通常用于深层嵌套的组件之间传递数据,避免繁琐的 “prop 逐级透传” (Prop Drilling)。
provide
(提供):在祖先组件中使用,提供一个值,可以被所有后代组件注入。inject
(注入):在任何后代组件中使用,注入一个由祖先组件提供的值。
使用方法:
// 祖先组件 (Provider)
import { provide, ref } from 'vue';
export default {
setup() {
const theme = ref('dark');
const toggleTheme = () => {
theme.value = theme.value === 'dark' ? 'light' : 'dark';
};
// 提供(provide)一个值(可以是响应式的)
provide('theme', theme); // 提供响应式数据
provide('toggleTheme', toggleTheme); // 提供方法
return { theme, toggleTheme };
}
};
// 后代组件 (Consumer)
import { inject } from 'vue';
export default {
setup() {
// 注入(inject)祖先组件提供的值
const theme = inject('theme', 'light'); // 第二个参数是默认值(可选)
const toggleTheme = inject('toggleTheme');
return {
theme,
toggleTheme
};
}
};
注意: 为了确保注入方和提供方之间的响应性链接,提供方应尽可能提供响应式数据(如 ref
或 reactive
)。
13. 如何在 Vue3 中使用事件总线 (Event Bus)?
Vue3 不再内置事件总线(因为 $on
, $off
等实例方法已被移除),但可以使用第三方库如 mitt
或 tiny-emitter
轻松实现。
使用 mitt
的步骤:
- 安装:
npm install mitt
- 创建事件总线实例:
// eventBus.js import mitt from 'mitt'; const emitter = mitt(); export default emitter;
- 在组件中发射 (emit) 事件:
// ComponentA.vue (发送事件) import emitter from './eventBus'; export default { setup() { const sendMessage = () => { emitter.emit('user-login', { username: 'alice' }); }; return { sendMessage }; } };
- 在组件中监听 (on) 事件:
// ComponentB.vue (接收事件) import emitter from './eventBus'; import { onUnmounted } from 'vue'; export default { setup() { const handleLogin = (user) => { console.log('User logged in:', user); }; // 监听事件 emitter.on('user-login', handleLogin); // 重要:在组件卸载时移除监听器,防止内存泄漏 onUnmounted(() => { emitter.off('user-login', handleLogin); }); } };
14. 什么是作用域插槽 (Scoped Slots)?
作用域插槽允许子组件将数据传递给它插槽中的内容,父组件可以决定如何渲染这些数据。这相当于子组件提供数据,父组件决定数据的呈现样式。
使用方法:
- 子组件:在
<slot>
元素上绑定要传递的属性(称为“插槽 props”)。 - 父组件:使用
v-slot
指令(或其简写#
)来接收插槽 props。
示例:
<!-- 子组件 MyList.vue -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<!-- 将 item 数据传递给插槽 -->
<slot name="item" :item="item" :index="index">
<!-- 默认内容,如果父组件没有提供则显示 -->
{{ item.name }}
</slot>
</li>
</ul>
</template>
<script>
export default {
props: ['items']
}
</script>
<!-- 父组件 -->
<template>
<MyList :items="userList">
<!-- 使用 v-slot:name="slotProps" 接收数据 -->
<template #item="{ item, index }">
<!-- 父组件可以自定义如何渲染每一项 -->
<span style="color: blue;">第{{ index + 1 }}位:{{ item.name }} - {{ item.age }}</span>
</template>
</MyList>
</template>
<script>
import MyList from './MyList.vue';
export default {
components: { MyList },
data() {
return {
userList: [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 }
]
};
}
};
</script>
15. 如何使用 expose
和 ref
实现组件通信?
在 Vue3 中,子组件可以使用 defineExpose
(在 <script setup>
中)或 expose
选项(在选项式 API 中)来显式暴露其内部的属性或方法。父组件则通过 ref 来获取子组件实例并调用这些暴露出来的方法。
步骤:
- 子组件:使用
defineExpose
暴露公共接口。 - 父组件:声明一个 ref 并将其绑定到子组件上,然后通过
ref.value
来访问子组件暴露的方法。
示例:
<!-- 子组件 ChildComponent.vue -->
<template>
<div>
<p>Count: {{ count }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
const count = ref(0);
const increment = () => {
count.value++;
};
// 暴露 count 和 increment 方法给父组件
defineExpose({
count,
increment
});
</script>
<!-- 父组件 ParentComponent.vue -->
<template>
<ChildComponent ref="childRef" />
<button @click="callChildMethod">Call Child's Method</button>
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
// 声明一个 ref 来引用子组件实例
const childRef = ref(null);
const callChildMethod = () => {
// 通过 ref 访问子组件暴露的方法
if (childRef.value) {
childRef.value.increment();
console.log('Child count is:', childRef.value.count);
}
};
</script>
这种方式使得父子组件间的通信更加明确和可控。
四、响应式系统进阶
16. 什么是响应式失效?如何解决?
响应式失效是指你修改了数据,但 Vue 无法检测到这个变化,因此视图没有相应更新。常见原因和解决方法如下:
常见原因及解决方案:
直接给响应式对象添加新属性:
const obj = reactive({ a: 1 }); obj.b = 2; // ❌ 添加新属性 b,不是响应式的
解决:
- 提前在
reactive
中声明所有属性。 - 使用
Object.assign
或扩展运算符创建新对象:obj = reactive({ ...obj, b: 2 }); // ✅
- 对
reactive
对象,可以使用obj.b = value
,因为 Proxy 支持。但对于ref
持有的对象,则需注意。
- 提前在
直接通过索引设置数组项或修改数组长度:
const arr = reactive([1, 2, 3]); arr[3] = 4; // ❌ 不是响应式的 arr.length = 2; // ❌ 不是响应式的
解决:
- 使用数组方法:
push
,pop
,shift
,unshift
,splice
,sort
,reverse
。 - 使用
arr.splice(index, 1, newValue)
替换元素。 - 重新赋值:
arr.value = [...arr.value, newItem]
(对于ref
数组)。
- 使用数组方法:
解构丢失响应性:
const state = reactive({ x: 1, y: 2 }); const { x, y } = state; // ❌ x, y 不再是响应式的
解决: 使用
toRefs
:const { x, y } = toRefs(state); // ✅ x, y 是响应式的 ref
17. Vue3 中的响应式代理有哪些局限性?
尽管 Proxy
很强大,但仍有一些边缘情况需要注意:
- 原始值:
Proxy
只能代理对象,不能代理原始值(如字符串、数字)。这就是为什么需要ref
来包装原始值。 - 相等性比较: 代理后的对象与原始对象不全等(
===
),但通过Proxy
访问属性与访问原始对象属性效果基本一致。 - 浏览器内置对象: 代理像
Map
,Set
,Date
,Promise
这样的内置对象可能需要额外处理,因为它们具有特殊的内部槽(internal slots),但 Vue3 内部已对这些做了处理。 - 性能开销: 对于非常大的、深度嵌套的对象,响应式转换会带来一定的性能开销。在极少数情况下,如果性能成为问题,可以考虑使用
shallowRef
或shallowReactive
。
18. 什么是 shallowReactive
和 shallowRef
?
shallowReactive
和 shallowRef
是 Vue3 提供的用于创建浅层响应式数据的 API,主要用于性能优化。
shallowReactive
: 创建一个响应式代理,但只跟踪对象第一层属性的响应性。深层嵌套的属性不会被自动转换为响应式。import { shallowReactive } from 'vue'; const state = shallowReactive({ count: 0, user: { name: 'Alice' } // ❌ user 内部的属性不是响应式的 }); state.count++; // ✅ 触发更新 state.user.name = 'Bob'; // ❌ 不会触发更新
shallowRef
: 创建一个 ref,但只跟踪其.value
本身的更改,不关心.value
内部的值(如果.value
是一个对象)。import { shallowRef } from 'vue'; const data = shallowRef({ count: 0 }); data.value = { count: 1 }; // ✅ 触发更新 (.value 被重新赋值) data.value.count++; // ❌ 不会触发更新 (内部属性变化)
使用场景: 当你有一个很大的对象或列表,但只有顶层属性的变化需要触发更新,或者你确信深层属性不会变化时,可以使用它们来避免不必要的深度响应式转换开销。
19. 什么是 readonly
和 shallowReadonly
?
readonly
和 shallowReadonly
用于创建只读的响应式代理,防止数据被意外修改,常用于传递 props 或上下文数据。
readonly
: 接受一个对象(响应式或普通)或 ref,返回一个原值的深层只读代理。任何尝试修改的操作都会被阻止。import { reactive, readonly } from 'vue'; const original = reactive({ count: 0 }); const copy = readonly(original); copy.count++; // ❌ 警告并阻止操作! original.count++; // ✅ 原对象依然可修改,copy.count 也会同步变为 1
shallowReadonly
: 创建一个浅层只读代理。只有第一层属性是只读的,深层属性仍然可以被修改。import { shallowReadonly } from 'vue'; const state = shallowReadonly({ count: 0, user: { name: 'Alice' } }); state.count++; // ❌ 警告并阻止操作! state.user.name = 'Bob'; // ✅ 可以修改深层属性(但不会触发视图更新,除非原对象是响应式的)
使用场景: 向子组件传递 props 时,Vue 内部会自动将其转换为只读的,但你也可以手动使用它们来明确表达数据不应被修改的意图。
20. 如何手动触发响应式更新?
在极少数情况下,你可能需要强制更新视图,即使依赖没有“正常”地变化。可以使用以下方法:
triggerRef(ref)
: 手动强制触发一个与shallowRef
关联的副作用。如果你修改了shallowRef
的深层属性,这很有用。import { shallowRef, triggerRef } from 'vue'; const shallowObj = shallowRef({ count: 0 }); shallowObj.value.count = 1; // 修改内部属性,不会触发更新 triggerRef(shallowObj); // ✅ 手动强制触发更新
重新赋值: 对于
ref
,直接给.value
赋予一个新对象是触发更新的最直接方式。const data = ref({ count: 0 }); data.value = { ...data.value, count: data.value.count + 1 }; // ✅ 触发更新
通常,你应该优先依赖 Vue 的自动响应式系统,手动触发更新应作为最后的手段。
五、虚拟 DOM 与渲染优化
21. Vue3 的虚拟 DOM 有哪些改进?
Vue3 对虚拟 DOM (Virtual DOM) 进行了重写,带来了显著的性能提升:
- 静态提升 (Static Hoisting): 编译时,Vue3 会将模板中的静态节点提升到渲染函数之外。这意味着这些静态节点只在首次渲染时创建一次,后续更新时会直接复用,避免了不必要的创建和比对开销。
- Patch Flags (补丁标志): 编译时,Vue3 会对动态节点进行分析,并为它们打上不同的
PatchFlag
(如TEXT
,PROPS
,CLASS
等)。在 diff 过程中,Vue 可以根据这些标志精确地知道需要检查哪些内容,从而跳过大量不必要的对比。例如,一个节点只有class
是动态的,那么更新时就只比对其class
,而忽略其子内容和属性。 - 树结构优化 (Tree Flattening): Vue3 会动态地记录包含动态节点的块(Block)。在更新时,它可以直接跳过这些块之外的纯静态内容,大大减少了需要遍历的节点数量。
- 事件缓存 (Cache Event Handlers): 内联的事件处理器(如
@click="handleClick"
)会被缓存起来,避免在每次渲染时重新创建函数,减少了内存开销和 GC 压力。
这些优化使得 Vue3 的 diff 算法比 Vue2 更加高效。
22. 什么是静态提升?
静态提升是 Vue3 编译阶段的一项重要优化。当编译器检测到模板中的纯静态内容(不包含任何动态绑定或指令的节点)时,会将这些静态节点的创建逻辑提升到渲染函数之外,成为一个常量。
效果:
- 这些静态节点只在应用首次渲染时被创建一次。
- 在后续的每次重新渲染中,它们都会被直接复用,而无需重新创建和比对。
- 这节省了创建虚拟 DOM 节点的开销,也减少了内存使用。
示例: 假设模板如下:
<template>
<div>
<span>This is a static header</span> <!-- 静态节点 -->
<p>{{ dynamicText }}</p> <!-- 动态节点 -->
</div>
</template>
编译后的渲染函数大致如下:
// 静态节点被提升到渲染函数外部,只创建一次
const _hoisted_1 = /*#__PURE__*/_createVNode("span", null, "This is a static header", -1 /* HOISTED */);
function render(_ctx, _cache) {
return _openBlock(), _createBlock("div", null, [
_hoisted_1, // 直接复用静态节点
_createVNode("p", null, _toDisplayString(_ctx.dynamicText), 1 /* TEXT */)
]);
}
23. 什么是 PatchFlag?
PatchFlag 是编译时添加到虚拟节点 (VNode) 上的一个数字标志,用于标识这个 VNode 有哪些类型的动态绑定或需要更新的部分。
作用: 在运行时 diff 算法中,Vue 可以根据 PatchFlag 快速判断出需要为这个节点做哪些更新操作,从而跳过大量不必要的属性或内容比较,极大提升了 diff 效率。
常见的 PatchFlag 类型:
export const enum PatchFlags {
TEXT = 1, // 动态文本内容
CLASS = 1 << 1, // 动态 class
STYLE = 1 << 2, // 动态 style
PROPS = 1 << 3, // 动态属性 (不包括 class/style)
FULL_PROPS = 1 << 4, // 表示需要完整 props 对比(例如 key 变化的节点)
HYDRATE_EVENTS = 1 << 5, // 带有事件监听器的节点
STABLE_FRAGMENT = 1 << 6, // 子节点顺序不会变化的 Fragment
KEYED_FRAGMENT = 1 << 7, // 带有 key 的 Fragment
UNKEYED_FRAGMENT = 1 << 8, // 子节点没有 key 的 Fragment
NEED_PATCH = 1 << 9, // 只需要非 props 的 patch
DYNAMIC_SLOTS = 1 << 10, // 动态插槽
HOISTED = -1, // 表示是静态提升的节点
BAIL = -2 // 表示 diff 算法应该完全退出这个节点及其子树的优化模式
}
例如,一个节点只有 :class
是动态的,它会被标记为 CLASS | 2
。在更新时,Vue 看到这个标志,就知道只需要检查它的 class
属性是否变化,而无需检查其子内容或其他属性。
24. 如何优化 Vue3 的渲染性能?
除了 Vue3 内置的优化,开发者也可以采取一些措施来提升应用性能:
合理使用
v-if
和v-show
:v-if
是真正的条件渲染,切换时元素会被销毁/重建,适用于运行时条件很少改变的场景。v-show
只是切换 CSS 的display
属性,初始渲染开销大,但切换开销小,适用于需要频繁切换的场景。
使用
computed
和watch
缓存: 充分利用computed
的缓存特性,避免在模板中执行复杂计算或方法调用。使用
v-once
: 用于只渲染一次的元素或组件,之后跳过更新。<span v-once>This will never change: {{ staticMessage }}</span>
使用
v-memo
: Vue3.2+ 新增指令,用于缓存一个模板的子树。只有当依赖数组中的值发生变化时,才会重新渲染。<div v-memo="[dependencyA, dependencyB]"> <!-- 大量内容,只有当 dependencyA 或 dependencyB 变化时才会更新 --> {{ dependencyA }} {{ dependencyB }} </div>
列表渲染优化:
- 始终提供唯一的
:key
。 - 对于超长列表,使用虚拟滚动技术(如
vue-virtual-scroller
),只渲染可视区域内的元素。
- 始终提供唯一的
减少不必要的响应式: 对于不需要响应式的数据,不要用
ref
或reactive
包裹。对于大型深层嵌套对象,考虑使用shallowRef
或shallowReactive
。组件懒加载/异步组件: 使用
defineAsyncComponent
延迟加载非首屏必需的组件,减少初始包大小。import { defineAsyncComponent } from 'vue'; const AsyncComp = defineAsyncComponent(() => import('./components/MyHeavyComponent.vue'));
使用
KeepAlive
: 缓存非活动组件实例,避免重复渲染。<KeepAlive> <component :is="currentComponent"></component> </KeepAlive>
25. 什么是 v-memo
指令?
v-memo
是 Vue 3.2 中引入的一个指令,用于缓存一个模板片段的虚拟 DOM 子树。它接受一个依赖数组。只有当数组中的某个值发生变化时,才会重新渲染该子树;否则,将复用之前的虚拟 DOM。
使用场景: 主要用于优化渲染性能非常昂贵的组件或大型 v-for
列表,在依赖明确且变化不频繁时非常有效。
示例:
<template>
<div v-for="user in users" :key="user.id" v-memo="[user.id, user.isActive]">
<!-- 这个复杂的子树只有在 user.id 或 user.isActive 变化时才会更新 -->
<p>{{ user.name }}</p>
<p>{{ user.email }}</p>
<p>{{ user.isActive ? 'Active' : 'Inactive' }}</p>
<!-- ... 很多其他内容 -->
</div>
</template>
在上例中,即使 users
数组的其他项发生了变化,或者父组件重新渲染,只要某个特定用户的 id
和 isActive
状态没变,其对应的 DOM 就会直接复用,避免了昂贵的虚拟 DOM 差异比对和真实 DOM 操作。
注意: 确保依赖数组包含所有在子模板中使用的、可能变化的响应式值。与 v-for
一起使用时,v-memo
需要放在同一个元素上。
六、组件与指令
26. Vue3 中的自定义指令有哪些变化?
Vue3 中的自定义指令的 API 与生命周期钩子更加一致,发生了一些变化:
Vue2 钩子 | Vue3 钩子 | 说明 |
---|---|---|
bind | beforeMount | 指令第一次绑定到元素时调用(Vue2的 bind 在 Vue3 中被 beforeMount 替代) |
inserted | mounted | 元素被插入到父 DOM 时调用。 |
update (已移除) | - | 在 Vue3 中被移除。组件更新前钩子逻辑应放在 beforeUpdate 中。 |
componentUpdated | updated | 所在组件及其子组件全部更新后调用。 |
unbind | unmounted | 指令与元素解绑时调用。 |
- | beforeUpdate (新增) | 在元素本身更新之前调用(类似组件的 beforeUpdate )。 |
Vue3 自定义指令示例:
// main.js
const app = createApp(App);
// 注册一个全局自定义指令 `v-focus`
app.directive('focus', {
// 当被绑定的元素挂载到 DOM 中时……
mounted(el) {
// 聚焦元素
el.focus();
},
// 在元素更新前调用(Vue3新增)
beforeUpdate(el, binding) {
// 可以在这里执行一些更新前的逻辑
},
// 组件更新后调用(Vue2的componentUpdated)
updated(el, binding) {
// ...
},
// 解绑时调用
unmounted(el) {
// 清理工作
}
});
<!-- 在模板中使用 -->
<input v-focus />
binding
参数: 在钩子函数中接收,包含以下属性:
value
: 传递给指令的值(如v-my-directive="value"
)。oldValue
: 之前的值,仅在beforeUpdate
和updated
中可用。arg
: 传递给指令的参数(如v-my-directive:foo
中的foo
)。modifiers
: 一个包含修饰符的对象(如v-my-directive.foo.bar
中的{ foo: true, bar: true }
)。instance
: 使用指令的组件实例。dir
: 指令的定义对象。
27. 什么是 Teleport
组件?它的使用场景是什么?
Teleport
是 Vue3 内置的一个组件,它可以将其插槽内容渲染到 DOM 中的另一个指定位置,而不是在当前组件的位置。
使用场景: 最常见的场景是处理模态框 (Modal)、弹出层 (Popover)、提示框 (Toast)、加载条 (Loading) 等需要脱离当前组件布局流、通常需要定位到 body
或其他特定容器下的组件。这样可以避免父组件的 CSS 样式(如 z-index
, overflow: hidden
, transform
)对弹出内容造成影响。
使用方法:
<template>
<div>
<button @click="showModal = true">Open Modal</button>
<!-- 使用 teleport 将模态框内容渲染到 body 元素下 -->
<Teleport to="body">
<div v-if="showModal" class="modal">
<p>This is a modal!</p>
<button @click="showModal = false">Close</button>
</div>
</Teleport>
</div>
</template>
<script setup>
import { ref } from 'vue';
const showModal = ref(false);
</script>
<style>
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 9999;
/* ... 其他样式 */
}
</style>
to
属性接受一个 CSS 选择器字符串或一个实际的 DOM 元素。它确保模态框的 HTML 结构最终位于 body
的末尾,从而避免受到父组件布局的干扰。
28. 什么是 Suspense
组件?如何使用它来处理异步组件?
Suspense
是 Vue3 内置的一个组件,用于优雅地处理异步组件依赖的加载状态,在异步组件解析完成之前显示一个回退(fallback)内容。
使用场景:
- 加载异步组件(通过
defineAsyncComponent
引入)。 - 组件内部有异步
setup()
函数(返回一个 Promise)。 - 配合 Composition API 的
async
/await
进行数据获取。
使用方法:
<template>
<Suspense>
<!-- #default 插槽:渲染异步组件 -->
<template #default>
<AsyncComponent />
</template>
<!-- #fallback 插槽:在异步组件加载完成前显示 -->
<template #fallback>
<div>Loading...</div> <!-- 可以是加载动画、骨架屏等 -->
</template>
</Suspense>
</template>
<script>
import { defineAsyncComponent } from 'vue';
// 定义一个异步组件
const AsyncComponent = defineAsyncComponent(() =>
import('./components/MyAsyncComponent.vue')
);
export default {
components: {
AsyncComponent
}
};
</script>
工作原理:
<Suspense>
会等待其默认插槽内的所有异步依赖(异步组件或async setup()
)都被解析。- 在等待期间,它渲染
#fallback
插槽的内容。 - 一旦所有异步依赖都解析完成,它就渲染默认插槽的内容。
错误处理: <Suspense>
组件本身不处理错误。你需要使用 onErrorCaptured
生命周期钩子或 errorCaptured
选项来捕获和处理异步错误。
29. 如何在 Vue3 中创建异步组件?
在 Vue3 中,可以使用 defineAsyncComponent
方法来创建异步组件,实现组件的懒加载。
基本用法:
import { defineAsyncComponent } from 'vue';
// 1. 工厂函数返回 Promise
const AsyncComp = defineAsyncComponent(() => {
return import('./components/MyAsyncComponent.vue'); // 动态 import 返回 Promise
});
// 2. 带有高级选项的对象形式
const AsyncCompWithOptions = defineAsyncComponent({
// 加载函数
loader: () => import('./MyAsyncComponent.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 在显示 loadingComponent 之前的延迟 | 默认值:200(单位 ms)
delay: 200,
// 如果提供了 timeout ,并且加载组件的时间超过了设定值,将显示错误组件
timeout: 3000,
// 定义组件是否可挂起 | 默认值:true
suspensible: false,
/**
*
* @param {*} error 错误信息对象
* @param {*} retry 一个函数,用于指示当 promise 加载器 reject 时,加载器是否应该重试
* @param {*} fail 一个函数,指示加载程序结束退出
* @param {*} attempts 允许的最大重试次数
*/
onError(error, retry, fail, attempts) {
if (error.message.match(/fetch/) && attempts <= 3) {
// 重试错误,最多3次
retry();
} else {
// 注意,retry/fail 就像 promise 的 resolve/reject 一样:
// 必须调用其中一个才能继续错误处理。
fail();
}
}
});
export default {
components: {
AsyncComp,
AsyncCompWithOptions
}
};
异步组件在打包时会被分割成单独的 chunk,只在需要时才会加载,有助于减小初始包体积,优化首屏加载速度。
30. 什么是递归组件?
递归组件是指组件在其自身模板中调用自身的组件。这在渲染具有嵌套或树形结构的数据时非常有用,例如评论列表、文件目录、级联选择器等。
在 Vue3 中使用递归组件:
- 全局组件: 全局注册的组件会自动在模板中可用,因此可以直接在自身模板中使用。
- 局部组件/
<script setup>
: 在<script setup>
中,组件无法通过名称引用自身,必须通过导入自身来实现递归。
示例:一个简单的树形组件
<!-- TreeItem.vue -->
<template>
<li>
<div>{{ item.name }}</div>
<ul v-if="item.children && item.children.length">
<!-- 关键:组件导入自身并使用 -->
<TreeItem
v-for="child in item.children"
:key="child.id"
:item="child"
/>
</ul>
</li>
</template>
<script setup>
// 必须导入自身
import TreeItem from './TreeItem.vue';
defineProps({
item: Object
});
</script>
<!-- ParentComponent.vue -->
<template>
<ul>
<TreeItem v-for="item in treeData" :key="item.id" :item="item" />
</ul>
</template>
<script setup>
import TreeItem from './TreeItem.vue';
const treeData = [/* ... 树形数据 ... */];
</script>
注意事项:
- 必须有一个明确的终止条件(如
v-if
),否则会导致无限递归和栈溢出。 - 确保每个递归实例都有一个唯一的
key
。 - 对于非常深的树,递归组件可能会带来性能考虑,此时可以考虑使用虚拟滚动等技术。
七、状态管理 (Pinia)
31. Pinia 和 Vuex 的区别是什么?
Pinia 是 Vue 官方推荐的下一代状态管理库,可以看作是 Vuex 5。它与 Vuex 4 的主要区别如下:
特性 | Vuex 4 | Pinia |
---|---|---|
API 风格 | Options API 风格 | Composition API 风格 |
mutations | 必须通过 mutations 同步修改状态 | 没有 mutations ,允许同步和异步直接修改状态 |
TypeScript 支持 | 支持,但需要一些额外配置 | 一流支持,提供出色的类型推断,API 设计非常 TypeScript 友好 |
模块化 | 使用 modules | 使用多个独立的 store,扁平化结构,支持自动代码分割 |
Store 实例 | 单一 store 实例 | 多个 store 实例 |
体积 | 稍大 | 非常轻量,压缩后约 1KB |
DevTools 支持 | 支持 | 支持 |
Pinia 的优势:
- 更简洁的 API: 去除了
mutations
和modules
的嵌套结构,学习成本和心智负担更低。 - 更好的 TS 支持: 类型推断几乎开箱即用。
- 更自然的组合: 使用 Composition API,可以像写组合式函数一样组织 store 的逻辑。
- 与 Vue3 生态融合更好: 是 Vue 官方生态的一部分。
32. 如何在 Pinia 中定义一个 store?
在 Pinia 中,一个 store 是通过 defineStore
函数定义的。它需要一个唯一名称作为第一个参数和一个选项对象(或一个 Setup 函数)。
选项式风格 (类似 Vuex):
// stores/counter.js
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
// 相当于 data
state: () => ({
count: 0
}),
// 相当于 computed
getters: {
doubleCount: (state) => state.count * 2
},
// 相当于 methods (可以同步或异步)
actions: {
increment() {
this.count++;
},
async incrementAsync() {
setTimeout(() => {
this.increment();
}, 1000);
}
}
});
组合式风格 (类似 Vue3 Setup):
// stores/counter.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useCounterStore = defineStore('counter', () => {
// state
const count = ref(0);
// getters
const doubleCount = computed(() => count.value * 2);
// actions
function increment() {
count.value++;
}
function asyncIncrement() {
setTimeout(() => {
increment();
}, 1000);
}
return { count, doubleCount, increment, asyncIncrement };
});
33. 如何在组件中使用 Pinia store?
首先,需要在应用层面安装 Pinia:
// main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
const pinia = createPinia();
const app = createApp(App);
app.use(pinia);
app.mount('#app');
然后,在组件中导入并使用 store:
<!-- MyComponent.vue -->
<template>
<div>
<p>Count: {{ counterStore.count }}</p>
<p>Double: {{ counterStore.doubleCount }}</p>
<button @click="counterStore.increment()">Increment</button>
<!-- 或者使用解构,但会失去响应性 -->
<p>Count (destructured): {{ count }}</p>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter';
import { storeToRefs } from 'pinia'; // 用于保持解构的响应性
const counterStore = useCounterStore();
// 直接解构会失去响应性!
// const { count, doubleCount } = counterStore; // ❌
// 使用 storeToRefs 可以保持响应性
const { count, doubleCount } = storeToRefs(counterStore); // ✅
</script>
Pinia 的 store 是一个 reactive 对象,所以可以直接访问其属性。要解构出 state 或 getters 并保持响应性,需要使用 storeToRefs()
。
34. Pinia 如何实现持久化存储?
Pinia 本身不提供持久化功能。要实现状态持久化(如存入 localStorage
),通常有两种方式:
手动在 store 中使用:
// stores/counter.js import { defineStore } from 'pinia'; export const useCounterStore = defineStore('counter', { state: () => ({ count: localStorage.getItem('count') ? parseInt(localStorage.getItem('count')) : 0 }), actions: { increment() { this.count++; // 每次修改后持久化 localStorage.setItem('count', this.count); } } });
使用插件: 社区有成熟的插件如
pinia-plugin-persistedstate
,可以更方便地实现。- 安装:
npm install pinia-plugin-persistedstate
- 使用:
// main.js import { createPinia } from 'pinia'; import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; const pinia = createPinia(); pinia.use(piniaPluginPersistedstate);
// stores/counter.js export const useCounterStore = defineStore('counter', { state: () => ({ count: 0 }), persist: true // 开启持久化 });
- 安装:
35. 如何在 Pinia 中处理异步操作?
在 Pinia 中,actions
既可以包含同步逻辑,也可以包含异步逻辑。处理异步操作非常简单自然。
示例:在 action 中发起异步请求
// stores/user.js
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
userData: null,
loading: false,
error: null
}),
actions: {
async fetchUser(userId) {
this.loading = true;
this.error = null;
try {
const response = await fetch(`/api/users/${userId}`);
this.userData = await response.json();
} catch (error) {
this.error = error.message;
} finally {
this.loading = false;
}
}
}
});
在组件中调用:
<template>
<div>
<div v-if="userStore.loading">Loading...</div>
<div v-else-if="userStore.error">Error: {{ userStore.error }}</div>
<div v-else>{{ userStore.userData }}</div>
<button @click="userStore.fetchUser(123)">Fetch User</button>
</div>
</template>
<script setup>
import { useUserStore } from '@/stores/user';
const userStore = useUserStore();
// 也可以在 onMounted 中调用
</script>
Pinia 的异步 action 使得管理异步状态(加载中、错误、数据)变得非常直观,无需额外的中间件。
八、路由 (Vue Router 4)
36. Vue Router 4 有哪些主要变化?
Vue Router 4 是专为 Vue 3 设计的版本,其主要变化包括:
- 创建方式: 使用
createRouter
函数而不是new Router()
。 - 模式定义:
mode: 'history'
改为history: createWebHistory()
(还有createWebHashHistory
和createMemoryHistory
)。 - 路由守卫: 导航守卫的 API 基本一致,但现在支持 Composition API 的
onBeforeRouteUpdate
和onBeforeRouteLeave
。 - 移除: 移除了
*
(星号或通配符)路由,必须使用自定义正则参数来捕获所有路由:/:catchAll(.*)
。 <router-view>
和<router-link>
: 用法基本不变,但现在支持 v-slot API。- Composition API 支持: 提供了
useRouter
,useRoute
等函数在setup
中访问路由和当前路由信息。
37. 如何在新项目中使用 Vue Router 4?
安装与配置:
- 安装:
npm install vue-router@4
- 创建路由器实例:
// router/index.js import { createRouter, createWebHistory } from 'vue-router'; import Home from '../views/Home.vue'; const routes = [ { path: '/', name: 'Home', component: Home }, { path: '/about', name: 'About', // 路由级代码分割,生成 about.[hash].js 块 component: () => import('../views/About.vue') } ]; const router = createRouter({ history: createWebHistory(process.env.BASE_URL), // 使用 history 模式 routes }); export default router;
- 在 main.js 中安装:
// main.js import { createApp } from 'vue'; import App from './App.vue'; import router from './router'; const app = createApp(App); app.use(router); app.mount('#app');
- 在 App.vue 中使用:
<!-- App.vue --> <template> <div id="app"> <nav> <router-link to="/">Home</router-link> | <router-link to="/about">About</router-link> </nav> <router-view /> </div> </template>
38. 如何在 setup
中使用 Vue Router?
在 setup
函数中,你可以使用 Composition API 提供的 useRouter
和 useRoute
函数来访问路由实例和当前路由信息。
useRouter()
: 返回路由器的实例,相当于选项式 API 中的this.$router
。用于编程式导航。useRoute()
: 返回一个当前路由地址的响应式对象,相当于选项式 API 中的this.$route
。包含params
,query
,hash
等信息。
示例:
<!-- Component.vue -->
<template>
<div>
<p>Current Route Path: {{ route.path }}</p>
<p>User ID: {{ route.params.id }}</p>
<button @click="goToAbout">Go to About</button>
</div>
</template>
<script setup>
import { useRouter, useRoute } from 'vue-router';
const router = useRouter();
const route = useRoute(); // 这是一个响应式对象
const goToAbout = () => {
// 编程式导航
router.push('/about');
// 或者 router.push({ name: 'About' });
};
// 监听路由参数变化
import { watch } from 'vue';
watch(() => route.params.id, (newId) => {
// 对参数变化做出响应...
});
</script>
39. Vue Router 4 的路由守卫有哪些?如何使用?
Vue Router 4 提供了多种路由守卫,用于控制导航:
全局守卫 (在路由器实例上定义):
router.beforeEach
: 全局前置守卫。router.beforeResolve
: 全局解析守卫。router.afterEach
: 全局后置钩子。
// router/index.js router.beforeEach((to, from, next) => { // ... next(); // 必须调用 next() 来解析这个钩子 });
路由独享的守卫 (在路由配置中定义):
beforeEnter
const routes = [ { path: '/admin', component: Admin, beforeEnter: (to, from, next) => { // 检查用户权限... next(); } } ];
组件内的守卫 (在组件选项中或
setup
中使用):onBeforeRouteUpdate
: Composition API 钩子,在当前路由改变但组件被复用时调用。onBeforeRouteLeave
: Composition API 钩子,在导航离开该组件的对应路由时调用。- 选项式 API 的
beforeRouteEnter
,beforeRouteUpdate
,beforeRouteLeave
仍然可用。
<script setup> import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'; onBeforeRouteUpdate(async (to, from, next) => { // 在当前路由改变,但是该组件被复用时调用 // 例如,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候 }); onBeforeRouteLeave((to, from, next) => { // 在导航离开渲染该组件的对应路由时调用 const answer = window.confirm('Do you really want to leave?'); if (answer) { next(); } else { next(false); } }); </script>
40. 如何实现路由懒加载?
路由懒加载是一种优化技术,它把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,从而减小初始包的体积。
在 Vue Router 4 中,使用动态 import
语法(也称为代码分割)来实现懒加载:
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
name: 'Home',
component: () => import(/* webpackChunkName: "home" */ '@/views/Home.vue') // 懒加载
},
{
path: '/about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ '@/views/About.vue') // 懒加载
},
{
path: '/user/:id',
name: 'User',
// 将组件按组分块,共享同一个 chunk
component: () => import(/* webpackChunkName: "user" */ '@/views/User.vue')
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;
注释 /* webpackChunkName: "home" */
是 webpack 特有的语法,用于为生成的 chunk 指定名称。使用 Vite 时,通常不需要这样做,但写了也无害。
结合 <Suspense>
组件,可以为异步加载的组件提供更好的用户体验。
九、其他高级特性与最佳实践
好的,我将为您整理50道常考的Vue3相关面试题及其答案。以下是前35道题的答案:
41. 什么是渲染函数 (Render Function)?如何使用?
渲染函数是 Vue 组件的一种高级用法,它允许开发者使用 JavaScript 代码来生成虚拟 DOM 节点,而不是使用模板语法。渲染函数提供了更大的灵活性和程序化控制能力。
使用方法: 在 Vue3 中,渲染函数通过 setup()
函数返回一个函数来定义,使用 h()
函数(hyperscript)创建虚拟节点。
import { h } from 'vue';
export default {
setup(props, context) {
// 返回渲染函数
return () => h('div', { class: 'container' }, [
h('h1', 'Hello Vue 3'),
h('p', `This is a render function example`),
h('button', {
onClick: () => console.log('Clicked!'),
class: 'btn'
}, 'Click me')
]);
}
}
使用场景:
- 需要高度动态的组件结构
- 实现复杂的逻辑控制
- 与第三方库集成(如 D3.js)
- 性能优化场景
42. h()
函数的参数有哪些?
h()
函数是创建虚拟 DOM 节点的核心函数,接受三个参数:
- type (必需):可以是字符串(HTML 标签名)、组件对象、异步组件函数或特殊组件(如 Fragment、Teleport)。
- props (可选):一个对象,包含节点的属性、样式、事件监听器等配置。
- children (可选):子节点,可以是字符串、数组或嵌套的
h()
调用。
// 基本用法
h('div', { class: 'container' }, 'Hello World')
// 带有事件和样式
h('button', {
class: 'btn-primary',
style: { color: 'red', fontSize: '14px' },
onClick: () => console.log('Clicked')
}, 'Click me')
// 嵌套子元素
h('div', { class: 'container' }, [
h('h1', 'Title'),
h('p', 'Paragraph content')
])
43. 渲染函数与模板的对比
特性 | 模板 (Template) | 渲染函数 (Render Function) |
---|---|---|
语法 | 类 HTML 声明式语法 | JavaScript 编程式语法 |
学习曲线 | 较平缓,易于上手 | 较陡峭,需要 JavaScript 知识 |
灵活性 | 受限,适用于常见场景 | 极高,可处理复杂逻辑 |
性能 | Vue3 编译优化后性能良好 | 手动优化可达到极致性能 |
类型支持 | 有限 | 更好的 TypeScript 支持 |
适用场景 | 大多数常规 UI 开发 | 动态组件、复杂逻辑、库开发 |
选择建议:
- 对于大多数应用场景,推荐使用模板,因其更直观、易维护。
- 只有在需要高度动态性、程序化控制或直接操作虚拟 DOM 的场景下,才考虑使用渲染函数。
44. 如何在渲染函数中实现条件渲染?
在渲染函数中,可以使用普通的 JavaScript 条件语句(如 if/else
、三元运算符)来实现条件渲染。
import { h } from 'vue';
export default {
setup(props) {
return () => {
// 条件逻辑
if (props.isLoading) {
return h('div', { class: 'loading' }, 'Loading...');
} else if (props.error) {
return h('div', { class: 'error' }, props.errorMessage);
} else {
return h('div', { class: 'content' }, [
h('h1', props.title),
h('p', props.content)
]);
}
};
}
}
使用三元运算符:
return () => h('div', [
h('h1', 'Title'),
props.showSubtitle ? h('h2', 'Subtitle') : null,
h('p', 'Content')
]);
45. 如何在渲染函数中实现列表渲染?
在渲染函数中,可以使用数组的 map()
方法来实现列表渲染。
import { h } from 'vue';
export default {
setup(props) {
return () => h('ul',
{ class: 'item-list' },
props.items.map(item => {
return h('li', {
key: item.id, // 重要:为每个列表项提供唯一的 key
class: 'item'
}, item.name);
})
);
}
}
带有复杂结构的列表:
return () => h('div',
{ class: 'user-list' },
props.users.map(user => {
return h('div', { key: user.id, class: 'user-card' }, [
h('img', { src: user.avatar, alt: user.name }),
h('h3', user.name),
h('p', user.email),
h('button', {
onClick: () => props.onUserSelect(user)
}, 'Select')
]);
})
);
46. 如何在渲染函数中处理事件?
在渲染函数中,可以通过 on
前缀来绑定事件处理函数。
import { h } from 'vue';
export default {
setup(props, { emit }) {
const handleClick = (event) => {
console.log('Clicked', event);
emit('button-clicked', event);
};
const handleInput = (event) => {
emit('input-updated', event.target.value);
};
return () => h('div', [
h('button', {
onClick: handleClick,
class: 'btn'
}, 'Click me'),
h('input', {
onInput: handleInput,
onFocus: () => console.log('Focused'),
onBlur: () => console.log('Blurred'),
value: props.value,
type: 'text'
})
]);
}
}
事件修饰符的处理: 在渲染函数中,事件修饰符(如 .stop
, .prevent
)需要手动实现:
const handleClick = (event) => {
// 手动实现 .stop.prevent
event.stopPropagation();
event.preventDefault();
emit('custom-event', event);
};
return () => h('button', {
onClick: handleClick
}, 'Click me');
47. 如何在渲染函数中使用插槽?
在渲染函数中,可以通过 context.slots
访问插槽内容。
import { h } from 'vue';
export default {
setup(props, context) {
return () => h('div', { class: 'card' }, [
// 默认插槽
context.slots.default ? context.slots.default() : h('p', 'Default content'),
// 具名插槽
context.slots.header ? context.slots.header() : null,
// 作用域插槽
context.slots.footer ? context.slots.footer({
timestamp: new Date()
}) : null
]);
}
}
在父组件中使用:
// 父组件
import { h } from 'vue';
import ChildComponent from './ChildComponent.vue';
export default {
setup() {
return () => h(ChildComponent, {
// 通过 scopedSlots 传递作用域插槽
scopedSlots: {
default: () => h('p', 'Main content'),
header: () => h('h1', 'Title'),
footer: (props) => h('p', `Footer at ${props.timestamp}`)
}
});
}
}
48. 如何在渲染函数中实现 v-model?
在渲染函数中,需要手动实现 v-model
的双向绑定逻辑。
import { h } from 'vue';
export default {
setup(props, { emit }) {
const handleInput = (event) => {
// 手动触发 update:modelValue 事件
emit('update:modelValue', event.target.value);
};
return () => h('input', {
value: props.modelValue,
onInput: handleInput,
type: 'text'
});
}
}
使用计算属性实现复杂逻辑:
import { h, computed } from 'vue';
export default {
setup(props, { emit }) {
const inputValue = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
});
const handleInput = (event) => {
inputValue.value = event.target.value;
};
return () => h('input', {
value: inputValue.value,
onInput: handleInput,
type: 'text'
});
}
}
49. 什么是 JSX?如何在 Vue3 中使用 JSX?
JSX 是一种 JavaScript 的语法扩展,允许在 JavaScript 代码中编写类似 HTML 的结构。在 Vue3 中使用 JSX 可以提供更直观的组件编写方式。
配置 JSX:
- 安装必要的依赖:
npm install @vitejs/plugin-vue-jsx -D
- 在 vite.config.js 中配置:
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
export default defineConfig({
plugins: [vue(), vueJsx()]
});
使用 JSX:
import { defineComponent, ref } from 'vue';
export default defineComponent({
setup() {
const count = ref(0);
const increment = () => {
count.value++;
};
return () => (
<div class="container">
<h1>JSX in Vue 3</h1>
<p>Count: {count.value}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
});
JSX 与渲染函数的对比:
- JSX 语法更接近模板,更易读易写
- JSX 提供了更好的类型支持(配合 TypeScript)
- JSX 在复杂组件中更有优势
- 渲染函数更灵活,适合高度动态的场景
50. 渲染函数的性能优化策略有哪些?
使用渲染函数时,可以采取以下性能优化策略:
使用
shallowRef
处理大型数据:import { shallowRef } from 'vue'; setup() { // 对于大型对象或数组,使用 shallowRef 减少响应式开销 const largeData = shallowRef([]); const updateData = (newData) => { largeData.value = Object.freeze(newData); // 冻结对象防止意外修改 }; return { largeData, updateData }; }
实现组件缓存:
import { h, keepAlive, defineAsyncComponent } from 'vue'; const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue') ); setup() { return () => h(keepAlive, [ h(AsyncComponent, { key: 'async-comp' }) ]); }
使用记忆化 (Memoization):
import { computed } from 'vue'; setup(props) { // 使用 computed 缓存计算结果 const expensiveValue = computed(() => { return props.data.map(item => complexCalculation(item)); }); return () => h('div', expensiveValue.value); }
分片渲染大规模数据:
import { h, ref, onMounted } from 'vue'; setup() { const visibleItems = ref([]); const allItems = ref([]); // 大规模数据集 const chunkRender = (items, chunkSize = 50) => { let index = 0; const renderChunk = () => { if (index >= items.length) return; const chunk = items.slice(index, index + chunkSize); index += chunkSize; // 使用 requestAnimationFrame 分批渲染 requestAnimationFrame(() => { visibleItems.value = [...visibleItems.value, ...chunk]; renderChunk(); }); }; renderChunk(); }; onMounted(() => { chunkRender(allItems.value); }); return () => h('div', visibleItems.value.map(item => h('div', { key: item.id }, item.name)) ); }
减少不必要的渲染:
import { h, shallowRef, watchEffect } from 'vue'; setup(props) { const optimizedData = shallowRef([]); watchEffect(() => { // 只在数据确实发生变化时更新 if (hasChanged(optimizedData.value, props.data)) { optimizedData.value = Object.freeze(props.data); } }); return () => h('div', optimizedData.value.map(item => h('div', { key: item.id }, item.name)) ); }
这些优化策略可以帮助提升渲染函数的性能,特别是在处理大型数据集或复杂组件时。
总结
Vue3 的渲染函数提供了强大的灵活性和控制能力,适用于需要高度动态或程序化 UI 的场景。通过 h()
函数,开发者可以直接创建和操作虚拟 DOM,实现比模板语法更复杂的逻辑。
关键要点:
- 渲染函数通过
h()
函数创建虚拟节点,接受 type、props 和 children 三个参数。 - 在条件渲染和列表渲染中,使用标准的 JavaScript 控制流语句。
- 事件处理通过
on
前缀的属性实现,需要手动处理事件修饰符。 - 插槽通过
context.slots
访问和处理。 v-model
需要手动实现双向绑定逻辑。- JSX 提供了更直观的语法,需要在构建工具中配置相应的插件。
- 性能优化策略包括使用
shallowRef
、组件缓存、记忆化和分片渲染等。