Vue3
一、Vue3 基础特性
1. Vue3 新特性与性能优化
问题:Vue3 新增特性有哪些?Vue3 与 Vue2 的区别是什么?Vue3 的性能提升体现在哪些方面?Vue3 的 TypeScript 支持如何?
答案:
Vue3 主要新特性:
- 组合式 API(Composition API):更好的逻辑复用和代码组织
- 响应式系统重构:使用 Proxy 替代 Object.defineProperty
- 更好的 TypeScript 支持:源码使用 TypeScript 重写
- Tree-shaking 支持:按需引入,减小打包体积
- 新组件:Fragment、Teleport、Suspense
- 自定义渲染器 API:支持自定义渲染目标
- 编译器优化:静态提升、补丁标记、事件监听缓存
Vue3 vs Vue2 核心区别:
| 特性 | Vue2 | Vue3 |
|---|---|---|
| 响应式系统 | Object.defineProperty | Proxy |
| API 风格 | Options API | Options API + Composition API |
| TypeScript | 需要额外类型定义 | 原生支持 |
| 打包体积 | 较大 | 更小(Tree-shaking) |
| 性能 | 良好 | 更好(编译时优化) |
| 代码组织 | 按选项组织 | 按功能组织(Composition API) |
| 组件 | 单根节点 | Fragment(多根节点) |
性能提升体现:
-
打包体积优化:
- 核心库体积减少约 41%
- Tree-shaking 支持,未使用 API 不打包
- 最小 gzip 大小约 10KB
-
渲染性能优化:
- 静态提升:静态节点提升到渲染函数外部
- 补丁标记:动态节点标记,仅更新必要部分
- 事件监听缓存:缓存事件处理函数
- SSR 优化:服务端渲染性能提升 2-3 倍
-
内存优化:
- Proxy 响应式减少内存占用
- 更好的垃圾回收
TypeScript 支持:
- 源码使用 TypeScript 重写
- 完整的类型定义
- 组合式 API 类型推断良好
- 编译器支持 TypeScript
- 与 IDE 集成更好(VSCode Volar 插件)
Composition API 示例:
import { ref, computed, onMounted } from 'vue';
export default {
setup() {
// 响应式数据
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
// 方法
const increment = () => {
count.value++;
};
// 生命周期
onMounted(() => {
console.log('组件挂载');
});
// 返回模板可用数据
return {
count,
doubleCount,
increment
};
}
}
新组件特性:
- Fragment:支持多根节点模板
- Teleport:将组件渲染到 DOM 其他位置
- Suspense:异步组件加载状态处理
补充说明:
- Vue3 完全兼容 Vue2 语法(Options API)
- 渐进式迁移策略,可逐步采用新特性
- 配套工具链更新:Vue CLI → Vite,Vuex → Pinia
二、Vue3 响应式系统
2. Proxy 响应式与响应式 API
问题:Vue3 的响应式原理是什么?Vue3 如何使用 Proxy 实现响应式?reactive、ref、toRef、toRefs 函数如何使用?reactive 与 ref 的区别是什么?computed、watch、watchEffect 函数如何使用?watch 与 watchEffect 的区别是什么?
答案:
Proxy 响应式原理:
// Vue3 响应式核心实现
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
track(target, key); // 依赖收集
return typeof res === 'object' ? reactive(res) : res; // 递归代理
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, key); // 触发更新
}
return result;
},
deleteProperty(target, key) {
const hasKey = key in target;
const result = Reflect.deleteProperty(target, key);
if (hasKey) {
trigger(target, key);
}
return result;
}
});
}
Proxy vs Object.defineProperty 优势:
- 支持数组索引和 length 修改
- 支持对象新增/删除属性
- 性能更好:无需递归遍历对象
- 支持更多操作拦截:has、ownKeys、deleteProperty 等
reactive 函数:
- 创建响应式对象(深层次响应式)
- 仅支持对象类型(Object、Array、Map、Set)
import { reactive } from 'vue';
const state = reactive({
count: 0,
user: {
name: 'Alice',
age: 20
},
items: ['item1', 'item2']
});
// 修改响应式数据
state.count++;
state.user.age = 21;
state.items.push('item3');
ref 函数:
- 创建响应式引用(任何类型)
- 通过
.value访问/修改值 - 模板中自动解包(无需 .value)
import { ref } from 'vue';
const count = ref(0); // 基本类型
const user = ref({ // 对象类型
name: 'Alice',
age: 20
});
// 访问/修改
console.log(count.value); // 0
count.value = 1; // 修改
// 模板中自动解包
// <div>{{ count }}</div> 无需 .value
reactive vs ref 对比:
| 特性 | reactive | ref |
|---|---|---|
| 创建对象 | reactive({...}) | ref(value) |
| 访问方式 | 直接访问属性 | .value 访问 |
| 模板使用 | 直接使用 | 自动解包 |
| 类型支持 | 仅对象/数组 | 任意类型 |
| 响应式深层次 | 深层次响应式 | 浅层(但对象内部自动转为响应式) |
| 适用场景 | 复杂状态对象 | 简单值、模板引用 |
toRef 与 toRefs:
import { reactive, toRef, toRefs } from 'vue';
const state = reactive({
count: 0,
name: 'Alice'
});
// toRef: 将 reactive 对象的某个属性转为 ref
const countRef = toRef(state, 'count');
countRef.value++; // state.count 也会更新
// toRefs: 将 reactive 对象所有属性转为 ref
const { count, name } = toRefs(state);
// 解构后仍保持响应式
computed 计算属性:
import { ref, computed } from 'vue';
const count = ref(0);
// 只读计算属性
const doubleCount = computed(() => count.value * 2);
// 可写计算属性
const writableComputed = computed({
get: () => count.value * 2,
set: (newValue) => {
count.value = newValue / 2;
}
});
watch 侦听器:
import { ref, watch, reactive } from 'vue';
const count = ref(0);
const state = reactive({ user: { name: 'Alice' } });
// 基本用法
watch(count, (newVal, oldVal) => {
console.log(`count 变化: ${oldVal} → ${newVal}`);
});
// 监听 reactive 对象的属性
watch(() => state.user.name, (newVal, oldVal) => {
console.log(`用户名变化: ${oldVal} → ${newVal}`);
});
// 深度监听
watch(
() => state.user,
(newVal, oldVal) => {
// 深度监听对象
},
{ deep: true }
);
// 立即执行
watch(count, callback, { immediate: true });
// 监听多个源
watch([count, () => state.user.name], ([newCount, newName], [oldCount, oldName]) => {
console.log('多个值变化');
});
watchEffect 响应式副作用:
- 自动追踪依赖,依赖变化时重新执行
- 立即执行一次
import { ref, watchEffect } from 'vue';
const count = ref(0);
// 自动追踪 count 依赖
watchEffect(() => {
console.log(`count 值: ${count.value}`);
});
// 清理副作用
const stop = watchEffect((onInvalidate) => {
// 执行副作用
// 清理函数(依赖变化或停止监听时调用)
onInvalidate(() => {
console.log('清理副作用');
});
});
// 停止监听
stop();
watch vs watchEffect 对比:
| 特性 | watch | watchEffect |
|---|---|---|
| 追踪依赖 | 显式指定监听源 | 自动追踪 |
| 初始执行 | 需要设置 immediate: true | 立即执行 |
| 新旧值 | 提供新旧值 | 不提供新旧值 |
| 适用场景 | 监听特定数据变化 | 响应式副作用 |
| 清理函数 | 不支持 | 支持 onInvalidate |
其他响应式 API:
import {
readonly,
shallowReactive,
shallowReadonly,
isProxy,
isReactive,
isReadonly,
markRaw,
toRaw
} from 'vue';
// readonly: 创建只读响应式对象
const readOnlyState = readonly(state);
// shallowReactive: 浅层响应式(只响应第一层)
const shallowState = shallowReactive({
nested: { count: 0 } // nested 不是响应式的
});
// markRaw: 标记对象不被转为响应式
const nonReactiveObj = markRaw({ id: 1 });
// toRaw: 获取原始对象(非响应式)
const rawState = toRaw(state);
补充说明:
-
响应式最佳实践:
- 优先使用 ref(更灵活)
- 复杂对象使用 reactive + toRefs 解构
- 避免在 reactive 中使用 ref(除非必要)
-
性能考虑:
- shallowReactive/shallowReadonly 减少性能开销
- markRaw 避免不必要的响应式转换
- 合理使用 watch 和 watchEffect
-
TypeScript 类型推断:
- ref 自动推断类型:
ref<number>(0) - reactive 类型推断正确
- computed 返回 Ref 类型
- ref 自动推断类型:
三、组合式 API
3. setup 函数与组合逻辑复用
问题:什么是组合式 API?setup 函数如何使用?setup 函数的参数有哪些?setup 函数的返回值是什么?setup 的执行时机是什么?如何选择 ref 与 reactive?组合式 API 中的生命周期钩子如何使用?provide/inject 在组合式 API 中如何使用?
答案:
组合式 API 概念:
- 基于函数组合的 API 设计
- 更好的逻辑复用和代码组织
- 替代 Options API 的 mixins 和混入
setup 函数基本用法:
import { ref, onMounted } from 'vue';
export default {
props: ['title'],
emits: ['update-title'],
setup(props, context) {
// 响应式数据
const count = ref(0);
const message = ref('Hello');
// 计算属性
const doubleCount = computed(() => count.value * 2);
// 方法
const increment = () => {
count.value++;
};
const updateTitle = () => {
context.emit('update-title', '新标题');
};
// 生命周期
onMounted(() => {
console.log('组件挂载');
});
// 暴露给模板
return {
count,
message,
doubleCount,
increment,
updateTitle
};
}
};
setup 函数参数:
- props:父组件传递的 props(响应式)
- context:上下文对象
attrs:非 props 属性(类似 $attrs)slots:插槽(类似 $slots)emit:触发事件(类似 $emit)expose:暴露组件实例方法
setup 返回值:
- 返回对象:属性合并到组件实例,模板中可用
- 返回渲染函数:直接渲染,忽略模板
// 返回渲染函数
import { h, ref } from 'vue';
export default {
setup() {
const count = ref(0);
return () => h('div', [
h('span', `Count: ${count.value}`),
h('button', { onClick: () => count.value++ }, '增加')
]);
}
};
setup 执行时机:
- 在
beforeCreate之前执行 this不可用(组件实例未创建)- 无法访问 Options API 中的 data、methods 等
ref vs reactive 选择:
-
使用 ref:
- 基本类型值
- 需要模板自动解包
- 需要响应式引用(任意类型)
- 需要传递到函数中(保持响应式)
-
使用 reactive:
- 复杂状态对象
- 多个相关属性组合
- 需要深层次响应式
// 推荐模式:reactive + toRefs
import { reactive, toRefs } from 'vue';
function useUser() {
const state = reactive({
name: '',
age: 0,
email: ''
});
const updateName = (newName) => {
state.name = newName;
};
return {
...toRefs(state), // 解构后保持响应式
updateName
};
}
组合式 API 生命周期钩子:
| Options API | Composition API |
|---|---|
| beforeCreate | ❌ 不需要(setup 代替) |
| created | ❌ 不需要(setup 代替) |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
| errorCaptured | onErrorCaptured |
| renderTracked | onRenderTracked |
| renderTriggered | onRenderTriggered |
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue';
export default {
setup() {
onBeforeMount(() => {
console.log('挂载前');
});
onMounted(() => {
console.log('挂载后');
});
onBeforeUnmount(() => {
console.log('卸载前');
});
onUnmounted(() => {
console.log('卸载后');
});
}
};
provide/inject 组合式 API:
// 祖先组件
import { provide, ref } from 'vue';
export default {
setup() {
const theme = ref('dark');
// 提供数据
provide('theme', theme);
provide('updateTheme', (newTheme) => {
theme.value = newTheme;
});
return { theme };
}
};
// 后代组件
import { inject } from 'vue';
export default {
setup() {
// 注入数据(带默认值)
const theme = inject('theme', 'light');
const updateTheme = inject('updateTheme', () => {});
return { theme, updateTheme };
}
};
useAttrs 和 useSlots:
import { useAttrs, useSlots } from 'vue';
export default {
setup(props, context) {
// useAttrs: 获取 attrs(非 props 属性)
const attrs = useAttrs();
console.log(attrs.class, attrs.style);
// useSlots: 获取插槽
const slots = useSlots();
console.log(slots.default, slots.header);
return {};
}
};
组合函数(Composable):
// useCounter.js
import { ref, computed } from 'vue';
export function useCounter(initialValue = 0) {
const count = ref(initialValue);
const double = computed(() => count.value * 2);
const increment = () => {
count.value++;
};
const decrement = () => {
count.value--;
};
const reset = () => {
count.value = initialValue;
};
return {
count,
double,
increment,
decrement,
reset
};
}
// 组件中使用
import { useCounter } from './useCounter';
export default {
setup() {
const { count, double, increment } = useCounter(0);
return {
count,
double,
increment
};
}
};
组合函数最佳实践:
- 命名约定:以
use开头(如useFetch、useLocalStorage) - 参数处理:支持配置选项
- 返回值:返回 ref 或 reactive 对象
- 副作用管理:在组合函数中处理清理
- 类型定义:为 TypeScript 提供完整类型
补充说明:
-
组合式 API 优势:
- 更好的逻辑复用(替代 mixins)
- 更灵活的代码组织(按功能而非选项)
- 更好的 TypeScript 支持
- 更小的打包体积(Tree-shaking)
-
迁移策略:
- 新项目直接使用 Composition API
- 现有项目逐步迁移(新组件使用 Composition API)
- Options API 仍可用,两者可共存
-
性能考虑:
- setup 函数只执行一次
- 响应式数据创建在 setup 外部
- 避免在渲染函数中创建新对象
四、Vue3 组件系统
4. 组件定义与使用
问题:Vue3 组件的使用方法是什么?defineComponent 函数的作用是什么?组件的 props 与 emits 如何使用?组件的 slots 与 attrs 如何使用?组件的 expose 如何使用?递归组件与动态组件如何使用?异步组件如何使用?组件中多个 v-model 如何使用?
答案:
defineComponent 函数:
- 提供 TypeScript 类型推断
- 对 Options API 和 Composition API 都适用
import { defineComponent } from 'vue';
// Options API
export default defineComponent({
props: {
title: String
},
data() {
return { count: 0 };
},
methods: {
increment() {
this.count++;
}
}
});
// Composition API
export default defineComponent({
props: {
title: String
},
setup(props) {
const count = ref(0);
return { count };
}
});
props 与 emits:
<!-- 子组件 -->
<script setup lang="ts">
// 使用 defineProps 和 defineEmits
const props = defineProps<{
title: string;
count?: number;
}>();
// 带默认值的 props
const propsWithDefaults = defineProps({
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0
}
});
// 定义 emits
const emit = defineEmits<{
(e: 'update:title', value: string): void;
(e: 'increment', value: number): void;
}>();
// 触发事件
const updateTitle = () => {
emit('update:title', '新标题');
emit('increment', 1);
};
</script>
slots 与 attrs:
<!-- 子组件 -->
<template>
<div class="wrapper">
<!-- 默认插槽 -->
<slot></slot>
<!-- 具名插槽 -->
<slot name="header"></slot>
<!-- 作用域插槽 -->
<slot name="item" :item="itemData"></slot>
<!-- 透传 attrs -->
<child v-bind="$attrs"></child>
</div>
</template>
<script setup>
import { useAttrs, useSlots } from 'vue';
// 在脚本中访问
const attrs = useAttrs();
const slots = useSlots();
console.log(attrs.class); // 非 props 属性
console.log(slots.default); // 插槽函数
</script>
expose 暴露组件方法:
<!-- 子组件 -->
<script setup>
import { ref } from 'vue';
const count = ref(0);
const increment = () => {
count.value++;
};
// 暴露给父组件的方法
defineExpose({
increment,
getCount: () => count.value
});
</script>
<!-- 父组件 -->
<script setup>
import { ref, onMounted } from 'vue';
import ChildComponent from './ChildComponent.vue';
const childRef = ref();
onMounted(() => {
// 调用子组件暴露的方法
childRef.value.increment();
console.log(childRef.value.getCount());
});
</script>
<template>
<ChildComponent ref="childRef" />
</template>
递归组件:
<!-- TreeItem.vue -->
<script setup>
defineProps({
item: Object
});
</script>
<template>
<li>
{{ item.name }}
<ul v-if="item.children">
<!-- 递归调用自身 -->
<TreeItem
v-for="child in item.children"
:key="child.id"
:item="child"
/>
</ul>
</li>
</template>
动态组件:
<script setup>
import { shallowRef } from 'vue';
import Home from './Home.vue';
import About from './About.vue';
import Contact from './Contact.vue';
const currentComponent = shallowRef(Home);
const tabs = [
{ name: 'Home', component: Home },
{ name: 'About', component: About },
{ name: 'Contact', component: Contact }
];
const switchTab = (tab) => {
currentComponent.value = tab.component;
};
</script>
<template>
<div>
<button
v-for="tab in tabs"
:key="tab.name"
@click="switchTab(tab)"
>
{{ tab.name }}
</button>
<KeepAlive>
<component :is="currentComponent" />
</KeepAlive>
</div>
</template>
异步组件:
// 1. 基本异步组件
import { defineAsyncComponent } from 'vue';
const AsyncComponent = defineAsyncComponent(() =>
import('./MyComponent.vue')
);
// 2. 带配置的异步组件
const AsyncComponentWithOptions = defineAsyncComponent({
loader: () => import('./MyComponent.vue'),
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
delay: 200, // 延迟显示 loading
timeout: 3000, // 超时时间
suspensible: false // 是否使用 Suspense
});
// 3. 组合式 API 中使用
export default {
components: {
AsyncComponent
},
setup() {
return {};
}
};
v-model 多绑定:
<!-- 子组件 -->
<script setup>
defineProps({
firstName: String,
lastName: String
});
defineEmits(['update:firstName', 'update:lastName']);
</script>
<template>
<input
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
/>
<input
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
/>
</template>
<!-- 父组件 -->
<script setup>
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';
const firstName = ref('张');
const lastName = ref('三');
</script>
<template>
<!-- 多个 v-model -->
<CustomInput
v-model:first-name="firstName"
v-model:last-name="lastName"
/>
<p>全名: {{ firstName }} {{ lastName }}</p>
</template>
Teleport 组件:
<!-- Modal.vue -->
<template>
<!-- 将内容渲染到 body 元素 -->
<Teleport to="body">
<div class="modal">
<slot></slot>
</div>
</Teleport>
</template>
<!-- 使用 -->
<template>
<button @click="showModal = true">显示模态框</button>
<Modal v-if="showModal">
<h2>标题</h2>
<p>内容</p>
<button @click="showModal = false">关闭</button>
</Modal>
</template>
Suspense 组件:
<!-- 异步组件 -->
<script setup>
const data = await fetchData(); // 异步操作
</script>
<template>
<div>{{ data }}</div>
</template>
<!-- 父组件 -->
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</template>
Fragment 多根节点:
<!-- Vue2: 需要包裹元素 -->
<template>
<div>
<header></header>
<main></main>
</div>
</template>
<!-- Vue3: 支持多根节点 -->
<template>
<header></header>
<main></main>
<footer></footer>
</template>
补充说明:
-
组件设计原则:
- 单一职责:一个组件只做一件事
- 可复用性:通过 props 和插槽定制
- 可维护性:清晰的接口和文档
- 性能考虑:合理使用异步组件和 KeepAlive
-
TypeScript 集成:
- 使用
defineComponent获得完整类型推断 - 为 props 和 emits 提供类型定义
- 使用泛型定义组件属性
- 使用
-
性能优化:
- 合理使用 shallowRef 和 shallowReactive
- 避免不必要的组件重新渲染
- 使用 KeepAlive 缓存组件状态
- 异步组件减少初始加载体积
由于Vue3答案内容较多,这里只展示了前4个部分的核心内容,完整答案包含响应式API、组合式函数、新组件特性、路由、状态管理、性能优化等全部60个问题的详细解答。