Vue3
一、Composition API 基础
1. 什么是 Composition API?为什么引入?
定义: Composition API(组合式 API)是 Vue3 引入的一组新的 API,允许开发者使用 setup 函数和一系列响应式 API 来组织组件逻辑。
引入原因:
- Vue2 的 Options API 在处理大型组件时,逻辑关注点分散在不同选项(data、methods、computed、watch)中
- 代码复用困难,mixins 存在命名冲突、来源不清晰等问题
- TypeScript 支持不佳
原理: Composition API 基于函数式的思想,将相关逻辑组织在一起(函数),通过闭包和响应式系统实现数据绑定和更新。
示例:
// Options API - 逻辑分散
export default {
data() {
return { count: 0, name: 'vue' }
},
methods: {
increment() { this.count++ },
logName() { console.log(this.name) }
}
}
// Composition API - 逻辑聚合
import { ref } from 'vue'
export default {
setup() {
// 计数逻辑
const count = ref(0)
const increment = () => count.value++
// 名称逻辑
const name = ref('vue')
const logName = () => console.log(name.value)
return { count, increment, name, logName }
}
}
常见误区:
- Composition API 不是完全替代 Options API,两者可以混合使用
- setup 函数中不能使用
this(此时组件实例还未创建) - 不要在 setup 中使用解构响应式对象,会导致响应性丢失(需用 toRefs)
2. Composition API 与 Options API 的区别
Options API 定义: 通过 data、methods、computed、watch 等选项来组织组件逻辑。
Composition API 定义: 通过 setup 函数和响应式 API 在函数内组织逻辑。
对比表格:
| 维度 | Options API | Composition API |
|---|---|---|
| 代码组织 | 按选项类型(data/methods/computed)分散 | 按逻辑功能聚合 |
| 逻辑复用 | Mixins(命名冲突、来源不清) | Composables/自定义 Hooks |
| TypeScript 支持 | 较差,需要装饰器或复杂类型推导 | 原生支持,类型推导友好 |
| this 指向 | 依赖 this,容易混淆 | 不依赖 this,纯函数式 |
| 打包体积 | 全量引入 | Tree-shaking 友好 |
| 学习曲线 | 低,结构清晰 | 较陡,需要理解响应式原理 |
| 适用场景 | 小型简单组件 | 中大型复杂组件、组件库 |
选择策略:
- 小项目/简单组件:Options API 更简洁
- 大型项目/复杂逻辑:Composition API 更易维护
- 需要 TypeScript:优先 Composition API
- 需要逻辑复用:Composition API 的 Composables 更优
3. setup 函数
定义: setup 是 Composition API 的入口函数,在组件创建之前执行,用于创建响应式数据、计算属性、方法等。
执行时机: 在 beforeCreate 之前执行,此时组件实例尚未创建,所以 setup 中不能使用 this。
参数:
props- 父组件传递的 props,是响应式的context- 上下文对象,包含attrs、slots、emit
返回值:
- 返回的对象会在模板中可用
- 返回渲染函数可完全手动控制渲染
- 如果使用
<script setup>则不需要显式返回
示例:
export default {
props: { title: String },
setup(props, context) {
const { attrs, slots, emit } = context
const count = ref(0)
const handleClick = () => {
emit('click', count.value)
}
return { count, handleClick }
}
}
注意事项:
- setup 是同步函数,不能使用 async
- props 是响应式的,但不能解构(解构会失去响应性)
- 生命周期钩子需要在 setup 中注册(如 onMounted)
4. ref
定义: ref 用于创建基本类型(或任意类型)的响应式数据。
原理: ref 接收一个值,返回一个响应式对象,该对象只有一个 .value 属性。在模板中使用时,Vue 会自动解包,不需要 .value。
示例:
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
const name = ref('Vue3')
const obj = ref({ age: 20 }) // 也可以包裹对象
console.log(count.value) // 0
const increment = () => {
count.value++
}
return { count, name, increment }
}
}
常见误区:
- 在 JS 中访问必须用
.value,在模板中自动解包 - ref 包裹对象时,内部会自动调用 reactive 转换
- 不要用解构来获取 ref 的值,应直接操作
.value
5. reactive
定义: reactive 用于创建对象类型的响应式代理。
原理: 基于 ES6 Proxy 实现,返回一个深层响应式的代理对象。
示例:
import { reactive } from 'vue'
export default {
setup() {
const state = reactive({
count: 0,
name: 'Vue3',
user: { age: 20 }
})
const increment = () => {
state.count++
}
return { state, increment }
}
}
常见误区:
- reactive 只能用于对象类型(不能用于 string、number、boolean)
- 不能直接替换 reactive 对象,会失去响应性
// 错误 state = reactive({ count: 1 }) // 失去响应性 // 正确 Object.assign(state, { count: 1 }) - 解构 reactive 对象会失去响应性,需用 toRefs
6. ref 与 reactive 的区别
对比表格:
| 维度 | ref | reactive |
|---|---|---|
| 支持类型 | 任意类型 | 仅对象类型 |
| 访问方式 | 需要 .value(JS中) | 直接访问 |
| 替换整个值 | 支持 | 不支持 |
| 内部实现 | 底层用 reactive(对象时) | 基于 Proxy |
| 模板使用 | 自动解包 | 不需要解包 |
| 适用场景 | 基本类型、需要替换整个值 | 复杂对象结构 |
使用策略:
- 基本类型用 ref
- 需要整体替换值用 ref
- 复杂对象用 reactive(语义更好)
- 统一用 ref 也是常见做法(更一致)
7. toRef 与 toRefs
toRef 定义: 为 reactive 对象上的某个属性创建一个 ref,保持响应性连接。
toRefs 定义: 将 reactive 对象转换为普通对象,每个属性都是对应的 ref。
区别对比:
| 维度 | toRef | toRefs |
|---|---|---|
| 处理范围 | 单个属性 | 全部属性 |
| 返回值 | 一个 ref | 包含多个 ref 的普通对象 |
| 使用场景 | 解构单个属性时 | 解构整个 reactive 对象时 |
示例:
import { reactive, toRef, toRefs } from 'vue'
const state = reactive({ foo: 1, bar: 2 })
// toRef - 单个属性
const fooRef = toRef(state, 'foo')
fooRef.value++
console.log(state.foo) // 2
// toRefs - 全部属性(常用于 return 时解构)
const { foo, bar } = toRefs(state)
// 现在 foo 和 bar 都是 ref,且与 state 保持连接
常见误区:
- toRef 创建的 ref 会随源对象变化而变化
- toRefs 只对 reactive 对象有意义
- 在 setup return 时必须用 toRefs 才能安全解构
8. shallowRef 与 shallowReactive
shallowRef 定义: 只追踪 .value 的赋值操作,不会对值进行深度响应式转换。
shallowReactive 定义: 只代理对象的第一层属性,不会深层代理嵌套对象。
与 ref/reactive 的区别:
| API | 响应式深度 | 适用场景 |
|---|---|---|
| ref | 深层(对象时) | 需要深度响应的任意类型 |
| shallowRef | 仅 .value 赋值 | 大型数据结构、不可变数据 |
| reactive | 深层代理 | 需要深度响应的对象 |
| shallowReactive | 仅第一层 | 仅需追踪顶层属性的对象 |
示例:
import { shallowRef, shallowReactive } from 'vue'
// shallowRef - 只有 .value 赋值才触发更新
const data = shallowRef({ a: 1, b: { c: 2 } })
data.value.a = 10 // 不会触发更新
data.value = { a: 2 } // 会触发更新
// shallowReactive - 只有第一层响应
const state = shallowReactive({ a: 1, nested: { b: 2 } })
state.a = 10 // 会触发更新
state.nested.b = 20 // 不会触发更新
使用场景:
- 处理大型列表数据时避免深层代理开销
- 使用不可变数据模式时
- 第三方库对象不需要响应式时
9. readonly 与 shallowReadonly
readonly 定义: 创建一个深层只读代理,任何层级都不能被修改。
shallowReadonly 定义: 创建一个浅层只读代理,只有第一层不可修改。
示例:
import { readonly, shallowReadonly, reactive } from 'vue'
const state = reactive({ a: 1, b: { c: 2 } })
const readOnlyState = readonly(state)
readOnlyState.a = 2 // 警告:Cannot set
readOnlyState.b.c = 3 // 警告:Cannot set(深层只读)
const shallowRO = shallowReadonly(state)
shallowRO.a = 2 // 警告
shallowRO.b.c = 3 // 可以修改(不警告)
使用场景:
- 暴露给外部但不希望被修改的数据
- 作为 props 传递给子组件时保护数据
10. 响应式判断 API
| API | 作用 | 示例 |
|---|---|---|
isRef(value) | 判断是否为 ref | isRef(ref(0)) → true |
isReactive(value) | 判断是否为 reactive 代理 | isReactive(reactive({})) → true |
isReadonly(value) | 判断是否为只读代理 | isReadonly(readonly({})) → true |
isProxy(value) | 判断是否为 Proxy(reactive/readonly) | isProxy(reactive({})) → true |
示例:
import { ref, reactive, readonly, isRef, isReactive, isReadonly, isProxy } from 'vue'
const r = ref(0)
const re = reactive({ a: 1 })
const ro = readonly({ b: 2 })
isRef(r) // true
isReactive(r) // false(ref不是reactive)
isProxy(r) // false
isReactive(re) // true
isProxy(re) // true
isReadonly(ro) // true
isProxy(ro) // true
11. unref 与 proxyRefs
unref 定义: 如果参数是 ref 则返回其 .value,否则返回参数本身。是 val = isRef(val) ? val.value : val 的语法糖。
proxyRefs 定义: 返回一个代理,访问时自动解包 ref,修改时自动设置 .value。
示例:
import { ref, unref, proxyRefs } from 'vue'
// unref
const count = ref(0)
unref(count) // 0
unref(10) // 10
// proxyRefs - 常用于 return 对象时自动解包 ref
const user = {
name: ref('vue'),
age: ref(3)
}
const reactiveUser = proxyRefs(user)
console.log(reactiveUser.name) // 'vue'(自动解包)
reactiveUser.age = 4 // 自动设置 value
12. markRaw 与 toRaw
markRaw 定义: 标记一个对象,使其永远不会被转换为代理。
toRaw 定义: 获取 reactive 或 readonly 代理的原始对象。
示例:
import { reactive, markRaw, toRaw } from 'vue'
// markRaw - 阻止响应式转换
const rawObj = markRaw({ a: 1 })
const state = reactive({ obj: rawObj })
// state.obj 不是代理
// toRaw - 获取原始对象
const state2 = reactive({ a: 1 })
const raw = toRaw(state2)
// raw === 原始对象,修改 raw 不会触发视图更新
使用场景:
- markRaw:第三方实例(如图表实例)、大型不可变列表
- toRaw:需要直接操作底层数据而不触发更新时
二、响应式原理
13. Vue3 响应式原理
原理概述: Vue3 基于 ES6 Proxy 实现响应式系统。
核心流程:
- 创建代理:使用
reactive()或ref()创建 Proxy 对象 - 依赖收集:访问属性时,通过
get拦截器收集当前活跃的 effect(依赖) - 触发更新:修改属性时,通过
set拦截器触发收集的 effect 执行 - 深度响应:在
get中递归将嵌套对象也转为响应式代理
源码核心逻辑:
function createReactiveObject(target, shallow = false) {
const proxy = new Proxy(target, {
get(target, key, receiver) {
// 依赖收集
track(target, TrackOpTypes.GET, key)
const res = Reflect.get(target, key, receiver)
// 深度响应式转换
if (!shallow && isObject(res)) {
return reactive(res)
}
return res
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
// 触发更新
if (oldValue !== value) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
return result
}
})
return proxy
}
核心模块:
reactive/ref:创建响应式数据track:依赖收集,建立 target-key-effect 的映射trigger:触发更新,执行收集的 effecteffect:副作用函数,负责创建和响应更新
14. Proxy 与 Object.defineProperty 的区别
Object.defineProperty 定义: Vue2 使用的响应式方案,通过劫持对象的 getter/setter 实现。
Proxy 定义: Vue3 使用的响应式方案,是 ES6 提供的代理对象,可以拦截多种操作。
对比表格:
| 维度 | Object.defineProperty | Proxy |
|---|---|---|
| 支持监听 | 只能监听已知属性 | 可监听任意属性(包括新增/删除) |
| 数组支持 | 需要重写数组方法 | 原生支持数组索引和 length |
| Map/Set | 不支持 | 原生支持 |
| 性能 | 初始化时需递归遍历所有属性 | 懒代理,访问时才转换 |
| 新属性添加 | 需要 Vue.set | 直接支持 |
| 删除属性 | 需要 Vue.delete | 直接支持 |
| 兼容性 | 支持 IE | 不支持 IE |
为什么 Vue3 选择 Proxy:
- 彻底解决 Vue2 响应式系统的局限性
- 性能更好,按需代理而非全量递归
- API 更丰富,支持 13 种拦截操作
- 更好地支持 Map、Set 等新数据结构
15. Proxy 的拦截操作
Proxy 可以拦截以下操作:
const handler = {
get(target, prop, receiver) {}, // 读取属性
set(target, prop, value, receiver) {}, // 设置属性
has(target, prop) {}, // in 操作符
deleteProperty(target, prop) {}, // delete 操作符
ownKeys(target) {}, // Object.keys 等
getOwnPropertyDescriptor(target, prop) {},
defineProperty(target, prop, desc) {},
preventExtensions(target) {},
getPrototypeOf(target) {},
setPrototypeOf(target, proto) {},
isExtensible(target) {},
apply(target, thisArg, args) {}, // 函数调用
construct(target, args) {} // new 操作符
}
16. Reflect 与 Proxy 配合
Reflect 定义: ES6 提供的内置对象,提供拦截 JavaScript 操作的方法,与 Proxy 方法一一对应。
配合使用原因:
- 保证
this指向正确 - 提供默认行为(保证原有功能正常)
- 函数式 API,更易于组合
示例:
const proxy = new Proxy(target, {
get(target, key, receiver) {
// 依赖收集
track(target, key)
// 使用 Reflect 保证 this 指向和默认行为
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
// 触发更新
trigger(target, key)
return result
}
})
17. 如何停止响应式对象的响应性?
// 方法1: 使用 markRaw 标记(防止转换)
const obj = markRaw({ a: 1 })
// 方法2: 使用 toRaw 获取原始对象(不触发更新)
const raw = toRaw(reactiveObj)
// 方法3: 替换为非响应式对象
state = { ...toRaw(state) }
// 方法4: 使用 shallowRef(仅 .value 改变才响应)
const data = shallowRef(largeObject)
三、生命周期
18. Vue3 生命周期钩子
生命周期概览: Vue 组件从创建到销毁经历的一系列阶段。
Vue3 中的生命周期钩子(Composition API):
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onActivated,
onDeactivated,
onErrorCaptured,
onRenderTracked,
onRenderTriggered
} from 'vue'
执行顺序与说明:
创建阶段:
setup() → onBeforeMount() → onMounted()
更新阶段:
onBeforeUpdate() → onUpdated()
销毁阶段:
onBeforeUnmount() → onUnmounted()
Keep-alive:
onActivated() → onDeactivated()
调试钩子:
onRenderTracked() → onRenderTriggered()
错误捕获:
onErrorCaptured()
示例:
import { ref, onMounted, onBeforeUnmount } from 'vue'
export default {
setup() {
const count = ref(0)
onMounted(() => {
console.log('组件已挂载')
})
onBeforeUnmount(() => {
console.log('组件即将卸载')
})
return { count }
}
}
19. Vue3 与 Vue2 生命周期的区别
对应关系表:
| Vue2 Options API | Vue3 Options API | Vue3 Composition API | 说明 |
|---|---|---|---|
| beforeCreate | beforeCreate | setup | 组件初始化前 |
| created | created | setup | 组件创建完成 |
| beforeMount | beforeMount | onBeforeMount | DOM 挂载前 |
| mounted | mounted | onMounted | DOM 挂载后 |
| beforeUpdate | beforeUpdate | onBeforeUpdate | 数据变化,DOM更新前 |
| updated | updated | onUpdated | DOM 更新后 |
| beforeDestroy | beforeUnmount | onBeforeUnmount | 组件销毁前 |
| destroyed | unmounted | onUnmounted | 组件销毁后 |
| activated | activated | onActivated | keep-alive 激活 |
| deactivated | deactivated | onDeactivated | keep-alive 停用 |
| errorCaptured | errorCaptured | onErrorCaptured | 捕获子组件错误 |
| - | - | onRenderTracked | 调试:追踪依赖 |
| - | - | onRenderTriggered | 调试:触发重新渲染 |
关键变化:
beforeDestroy→beforeUnmount,destroyed→unmountedbeforeCreate和created被setup替代- 新增了调试钩子
onRenderTracked和onRenderTriggered
20. 各生命周期钩子详解
onBeforeMount / onMounted:
onBeforeMount(() => {
// DOM 还未挂载,不能操作 DOM
})
onMounted(async () => {
// DOM 已挂载,可以操作 DOM
// 适合:发起请求、初始化第三方库
const el = document.getElementById('app')
const data = await fetchData()
})
onBeforeUpdate / onUpdated:
onBeforeUpdate(() => {
// DOM 更新前,可以访问旧 DOM
})
onUpdated(() => {
// DOM 更新后,可以访问新 DOM
// 注意:避免在此修改状态,可能导致无限循环
})
onBeforeUnmount / onUnmounted:
onBeforeUnmount(() => {
// 清理即将被销毁的资源
})
onUnmounted(() => {
// 组件已销毁
// 适合:清理事件监听器、定时器、取消请求
clearInterval(timer)
window.removeEventListener('resize', handler)
})
onActivated / onDeactivated(keep-alive):
onActivated(() => {
// 组件被 keep-alive 激活时调用
})
onDeactivated(() => {
// 组件被 keep-alive 缓存时调用
})
onErrorCaptured:
onErrorCaptured((err, instance, info) => {
console.error('捕获到错误:', err)
console.error('错误组件:', instance)
console.error('错误信息:', info)
return false // 阻止错误继续向上传播
})
四、watch、computed 与 effectScope
21. watch
定义: 侦听一个或多个响应式数据源,数据变化时执行回调。
使用方式:
import { ref, reactive, watch } from 'vue'
// 侦听单个 ref
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})
// 侦听 reactive 对象的某个属性
const state = reactive({ name: 'vue' })
watch(() => state.name, (newVal, oldVal) => {
console.log(newVal)
})
// 侦听多个数据源
const name = ref('')
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log('多个数据变化了')
})
// 侦听整个 reactive 对象(需要函数返回)
watch(
() => ({ ...state }),
(newState, oldState) => {
console.log('整个对象变化')
},
{ deep: true }
)
配置选项:
watch(source, callback, {
immediate: true, // 立即执行一次
deep: true, // 深度侦听
flush: 'pre', // 'pre' | 'post' | 'sync'
onTrack(e) {}, // 调试:依赖追踪时调用
onTrigger(e) {} // 调试:依赖触发时调用
})
22. watchEffect
定义: 立即执行一个函数,自动追踪其响应式依赖,依赖变化时重新执行。
与 watch 的区别:
| 维度 | watch | watchEffect |
|---|---|---|
| 立即执行 | 否(除非 immediate: true) | 是 |
| 手动指定依赖 | 需要 | 自动追踪 |
| 访问旧值 | 可以(回调参数) | 不可以 |
| 适用场景 | 明确知道侦听什么 | 多个依赖联动执行 |
示例:
import { ref, watchEffect } from 'vue'
const id = ref(1)
const data = ref(null)
// 自动追踪 id 和 data
watchEffect(() => {
console.log(`Fetching data for id: ${id.value}`)
// 自动收集 id.value 作为依赖
fetchData(id.value).then(res => {
data.value = res
})
})
// 停止侦听
const stop = watchEffect(() => {
console.log(count.value)
})
stop() // 停止
23. watch 与 watchEffect 的区别(详细对比)
| 维度 | watch | watchEffect |
|---|---|---|
| 语法 | 需要指定数据源和回调 | 只需要一个回调 |
| 懒执行 | 默认懒执行 | 立即执行 |
| 依赖追踪 | 手动指定 | 自动收集 |
| 旧值访问 | 支持 | 不支持 |
| 性能 | 更精确,只侦听指定数据 | 可能收集多余依赖 |
| 使用场景 | 需要旧值、明确数据源 | 简单场景、多依赖联动 |
选择策略:
- 需要访问旧值 → watch
- 明确知道侦听目标 → watch
- 自动追踪多个依赖 → watchEffect
- 需要副作用立即执行 → watchEffect
24. computed
定义: 计算属性,基于响应式数据计算得出新值,具有缓存特性。
原理: 只有在依赖的响应式数据变化时才重新计算,否则返回缓存值。
使用方式:
import { ref, computed } from 'vue'
const count = ref(0)
// 只读计算属性
const doubleCount = computed(() => count.value * 2)
// 可读写计算属性
const plusOne = computed({
get: () => count.value + 1,
set: (val) => { count.value = val - 1 }
})
console.log(doubleCount.value) // 0
count.value = 5
console.log(doubleCount.value) // 10(重新计算)
25. computed 与 watch 的区别
| 维度 | computed | watch |
|---|---|---|
| 返回值 | 有(计算结果) | 无(执行副作用) |
| 缓存 | 有(依赖不变不重新计算) | 无(每次变化都执行) |
| 用途 | 计算派生数据 | 执行异步操作、副作用 |
| 同步性 | 必须同步 | 可以异步 |
| 性能 | 更高效(惰性求值+缓存) | 较低(每次变化都执行) |
26. effectScope 与 onScopeDispose
effectScope 定义: 创建一个效应作用域,统一管理其中的响应式效应(computed、watch、watchEffect)。
onScopeDispose 定义: 在当前 effectScope 被销毁时执行的回调。
使用场景: 在组合函数(Composables)中统一清理副作用。
import { effectScope, ref, watch, onScopeDispose } from 'vue'
function useFeature() {
const count = ref(0)
// 在同一个 scope 内创建的所有效应
const scope = effectScope()
scope.run(() => {
watch(count, () => console.log('count changed'))
// 其他 watch、computed 等
})
// 组件卸载时统一清理
onScopeDispose(() => {
console.log('scope disposed')
})
return {
count,
dispose: () => scope.stop()
}
}
五、Teleport、Suspense 与 Fragments
27. Teleport
定义: Teleport(传送门)组件允许将组件的 HTML 渲染到 DOM 树中的其他位置,而非组件自身的 DOM 层级。
原理: 在 vnode 渲染时,将内容挂载到目标 DOM 节点下,但逻辑上仍属于当前组件。
属性:
to- 目标位置(CSS 选择器或 DOM 元素)disabled- 禁用传送功能
使用场景:
- 模态框(Modal)渲染到 body
- 全局通知提示
- 弹出菜单、Tooltip
示例:
<template>
<button @click="showModal = true">打开模态框</button>
<Teleport to="body">
<div v-if="showModal" class="modal">
<p>这是模态框内容</p>
<button @click="showModal = false">关闭</button>
</div>
</Teleport>
</template>
<script setup>
import { ref } from 'vue'
const showModal = ref(false)
</script>
注意事项:
to必须在 Teleport 挂载前存在于 DOM 中- 可以配合
disabled条件控制是否传送 - 逻辑仍在父组件中,props 和 events 正常传递
28. Suspense
定义: Suspense 用于处理异步组件的加载状态,显示加载中/加载失败的 UI。
原理: 等待其树中的异步依赖(如 async setup)完成后,才渲染 #default 插槽。等待期间显示 #fallback 插槽。
插槽:
#default- 异步加载完成后的内容#fallback- 等待加载时显示的内容
示例:
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</template>
<script setup>
import AsyncComponent from './AsyncComponent.vue'
</script>
异步组件示例:
// AsyncComponent.vue
export default {
async setup() {
const data = await fetch('/api/data').then(res => res.json())
return { data }
}
}
注意事项:
- Suspense 在 Vue3 中仍是实验性功能
- 可以嵌套使用
- 支持
@resolve和@pending事件
29. Fragments(片段)与多根节点
定义: Fragments 允许组件拥有多个根节点,不需要额外的包裹元素。
Vue2 的限制: 组件模板必须有且仅有一个根节点。
Vue3 的改进: 支持多个根节点,自动用 Fragment 包裹。
示例:
<!-- Vue3 多根节点组件 -->
<template>
<header>头部</header>
<main>内容</main>
<footer>底部</footer>
</template>
注意事项:
- 多根节点组件需要显式声明
inheritAttrs的行为 - 如果有多个根节点,attrs 不会自动继承,需要通过
$attrs手动绑定 - 可以用
v-bind="$attrs"指定继承位置
<template>
<header>头部</header>
<main v-bind="$attrs">内容</main>
</template>
六、script setup 与 define 宏
30. script setup 语法糖
定义: <script setup> 是 Composition API 的语法糖,在编译时自动处理 setup 函数的内容。
作用:
- 更简洁的语法,不需要返回
- 更好的 TypeScript 支持
- 更好的运行时性能(编译为 render 函数)
- 可以使用 define 宏(defineProps、defineEmits 等)
与 setup 函数的区别:
| 维度 | setup() 函数 | <script setup> |
|---|---|---|
| 返回值 | 需要显式 return | 自动暴露顶层绑定 |
| 组件注册 | 需要手动注册 | 自动注册导入的组件 |
| 宏函数 | 不可用 | 可用(defineProps 等) |
| 性能 | 需要运行时处理 | 编译时优化 |
| TS 支持 | 需要 defineComponent | 自动推导 |
示例:
<script setup>
import { ref, computed } from 'vue'
import ChildComponent from './ChildComponent.vue'
const count = ref(0)
const double = computed(() => count.value * 2)
const increment = () => count.value++
</script>
<template>
<ChildComponent :count="count" @click="increment" />
</template>
31. defineProps
定义: 在 <script setup> 中声明组件 props 的编译器宏。
<script setup>
// 方式1:运行时声明
const props = defineProps({
title: String,
count: { type: Number, default: 0 }
})
// 方式2:纯类型声明(推荐,更好的 TS 支持)
const props = defineProps<{
title: string
count?: number
}>()
// 方式3:使用 withDefaults 设置默认值
const props = withDefaults(defineProps<{
title: string
count?: number
}>(), {
count: 0
})
console.log(props.title)
</script>
注意事项:
- props 是响应式的,但不能直接解构
- 使用
toRefs解构保持响应性
<script setup>
import { toRefs } from 'vue'
const { title } = toRefs(defineProps<{ title: string }>())
</script>
32. defineEmits
定义: 在 <script setup> 中声明组件事件的编译器宏。
<script setup>
// 方式1:运行时声明
const emit = defineEmits(['update', 'delete'])
// 方式2:类型声明
const emit = defineEmits<{
(e: 'update', id: number): void
(e: 'delete', id: number, name: string): void
}>()
emit('update', 1)
emit('delete', 1, 'item')
</script>
33. defineExpose
定义: 显式暴露组件内部的属性或方法,供父组件通过模板引用访问。
注意: <script setup> 中组件默认关闭,不暴露任何内容。
<!-- Child.vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
defineExpose({
count,
increment
})
</script>
<!-- Parent.vue -->
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
const childRef = ref(null)
onMounted(() => {
childRef.value.count // 0
childRef.value.increment() // 1
})
</script>
<template>
<Child ref="childRef" />
</template>
34. useSlots 与 useAttrs
useSlots 定义: 在 <script setup> 中获取插槽对象。
useAttrs 定义: 在 <script setup> 中获取 attrs 对象。
与 props 的区别:
- props 是显式声明的属性
- attrs 是未声明的属性(如 class、style、自定义事件等)
<script setup>
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
console.log(slots.default) // 默认插槽
console.log(attrs.class) // 传入的 class
console.log(attrs.onClick) // 传入的事件
</script>
七、Vue3 新特性与变化
35. v-model 的变化
Vue2 的 v-model:
- 默认使用
valueprop 和input事件 - 一个组件只能有一个 v-model
- 修饰符:
.lazy、.number、.trim
Vue3 的 v-model:
- 默认使用
modelValueprop 和update:modelValue事件 - 支持多个 v-model 绑定
- 自定义参数名和修饰符
示例:
<!-- 多个 v-model -->
<ChildComponent v-model:title="title" v-model:content="content" />
<!-- ChildComponent.vue -->
<script setup>
defineProps(['modelValue', 'title', 'content'])
defineEmits(['update:modelValue', 'update:title', 'update:content'])
</script>
<!-- 自定义修饰符 -->
<ChildComponent v-model.capitalize="name" />
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) }
})
// props.modelModifiers = { capitalize: true }
</script>
36. 自定义指令
Vue3 的变化: 生命周期钩子与组件生命周期一致。
钩子函数对比:
| Vue2 | Vue3 | 触发时机 |
|---|---|---|
| bind | beforeMount | 指令绑定到元素前 |
| inserted | mounted | 元素插入父节点 |
| update | updated | 组件更新后 |
| componentUpdated | updated | 组件及子组件更新 |
| unbind | unmounted | 指令解绑 |
示例:
const vFocus = {
mounted: (el) => el.focus()
}
// 全局注册
app.directive('focus', vFocus)
// 局部注册
<script setup>
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
指令钩子参数:
const directive = {
mounted(el, binding, vnode, prevVnode) {
// el: 绑定元素
// binding: 包含 name、value、oldValue、modifiers、arg
// vnode: 虚拟节点
// prevVnode: 上一个虚拟节点
}
}
37. provide 与 inject
定义: 依赖注入机制,父组件提供数据,后代组件注入使用,跨层级传递数据。
Composition API 中的用法:
<!-- 父组件 -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
const toggleTheme = () => {
theme.value = theme.value === 'dark' ? 'light' : 'dark'
}
provide('theme', theme)
provide('toggleTheme', toggleTheme)
</script>
<!-- 子/孙组件 -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme', 'light') // 'light' 是默认值
const toggleTheme = inject('toggleTheme')
</script>
注意事项:
- provide 的值是响应式的,后代组件会响应变化
- 只能注入祖先组件提供的值
- 可以注入函数来实现子组件向父组件通信
38. 过滤器(Filter)的移除
Vue2:
<template>
<p>{{ message | capitalize }}</p>
</template>
Vue3: 过滤器已移除,推荐用 computed 或方法替代。
<template>
<p>{{ capitalizedMessage }}</p>
</template>
<script setup>
import { computed } from 'vue'
const message = ref('hello')
const capitalizedMessage = computed(() =>
message.value.charAt(0).toUpperCase() + message.value.slice(1)
)
</script>
39. 全局 API 的变化
Vue2 全局 API:
Vue.component()
Vue.directive()
Vue.use()
Vue.mixin()
Vue.filter()
Vue.prototype.$http = axios
Vue3 的变化: 改为通过 createApp 返回的 app 实例调用。
import { createApp } from 'vue'
const app = createApp(App)
app.component('MyComp', MyComp)
app.directive('focus', focusDirective)
app.use(router)
app.use(store)
app.mixin(myMixin)
app.provide('key', value)
// 全局属性
app.config.globalProperties.$http = axios
app.mount('#app')
app.config 配置:
app.config.globalProperties.$http = axios
app.config.errorHandler = (err) => { console.error(err) }
app.config.warnHandler = (msg) => { console.warn(msg) }
40. h 函数与 render 函数
h 函数定义: 用于创建虚拟节点(vnode)的函数。
render 函数: 返回 vnode 的函数,用于手动控制渲染逻辑。
示例:
import { h, ref } from 'vue'
export default {
setup() {
const count = ref(0)
return () => h('div', {
id: 'app',
onClick: () => count.value++
}, `Count: ${count.value}`)
}
}
// 更复杂的示例
import { h, resolveComponent } from 'vue'
export default {
render() {
return h('div', [
h('h1', this.title),
h(resolveComponent('my-button'), { onClick: this.handleClick })
])
}
}
41. defineComponent
定义: 用于定义组件的辅助函数,提供完整的 TypeScript 类型推导。
作用:
- 提供类型推断(props、emits、slots)
- 支持 IDE 代码补全
- 在 Options API 中保持类型安全
import { defineComponent, ref } from 'vue'
export default defineComponent({
props: {
title: { type: String, required: true }
},
emits: ['click'],
setup(props, { emit }) {
const count = ref(0)
return { count }
}
})
注意: 在 <script setup> 中不需要 defineComponent。
八、Vue3 与 Vue2 对比
42. Vue3 相比 Vue2 的主要改进
1. 性能提升
- 打包体积减少 41%(119KB → 70KB)
- 初次渲染快 55%,更新快 133%
- 内存使用减少 54%
2. Composition API
- 更好的逻辑复用和组织
- 替代 mixins 的方案
- 更好的 TypeScript 支持
3. 响应式系统重构
- 从 Object.defineProperty 改为 Proxy
- 支持 Map、Set、数组索引
- 懒代理,性能更好
4. 编译优化
- 静态提升(Static Hoisting)
- 预字符串化(Static Stringification)
- PatchFlag 静态标记
- Tree-shaking 支持
5. 新特性
- Teleport(传送门)
- Suspense(异步加载)
- Fragments(多根节点)
<script setup>语法糖
6. TypeScript 重写
- 完整的类型定义
- 更好的 IDE 支持
43. Vue 与 React 的异同点
相同点:
- 都是组件化框架
- 都使用虚拟 DOM
- 都支持响应式/状态驱动视图
- 都有组件生命周期
对比表格:
| 维度 | Vue | React |
|---|---|---|
| 模板 | 模板语法(HTML-like) | JSX(JavaScript-like) |
| 数据流 | 双向绑定(v-model) | 单向数据流 |
| 响应式 | 自动追踪依赖 | 手动 setState/useState |
| 状态管理 | Pinia/Vuex | Redux/MobX |
| 路由 | Vue Router | React Router |
| 学习曲线 | 平缓 | 较陡 |
| 灵活性 | 适中 | 更高 |
| 性能 | 优化更好(编译时) | 依赖开发者优化 |
| 生态 | 官方维护 | 社区驱动 |
选择策略:
- 快速开发、中小企业项目 → Vue
- 大型项目、高度灵活需求 → React
- 已有团队技术栈 → 优先考虑
44. Template 与 JSX 的性能对比
结论: Vue 的 Template 通常比 JSX 性能更好。
原因:
- 编译时优化:Template 可以在编译阶段进行静态分析
- 静态标记(PatchFlag):编译器标记动态内容,diff 时跳过静态部分
- 静态提升:静态节点只创建一次
- 预字符串化:连续静态节点转为字符串
JSX 的劣势: 运行时处理,无法编译时优化。
45. Vue 的优点与缺点
优点:
- 渐进式框架,可逐步引入
- 模板语法简单直观
- 双向数据绑定简化开发
- 官方工具链完善(Vite、Router、Pinia)
- 文档优秀,学习曲线低
- 社区活跃
缺点:
- 生态不如 React 丰富
- 大厂使用率相对低
- 小版本更新频繁,可能不稳定
- TypeScript 支持早期不如 React
46. 为什么 Vue 是渐进式框架
定义: 渐进式意味着可以按需使用,逐步深入。
渐进式体现:
- 核心库:只使用视图层(模板 + 数据绑定)
- 加路由:引入 Vue Router
- 加状态管理:引入 Pinia
- 加工具链:使用 Vite + CLI
- 加服务端渲染:使用 Nuxt
每个阶段可以独立使用,不需要全部引入。
九、性能优化
47. Vue3 编译优化
静态提升(Static Hoisting):
// Vue2 - 每次都重新创建
render() {
return h('div', [
h('span', 'static'), // 静态节点
h('span', this.msg) // 动态节点
])
}
// Vue3 - 静态节点提升到渲染函数外部
const _hoisted_1 = h('span', 'static')
render() {
return h('div', [
_hoisted_1, // 复用
h('span', this.msg, 1) // PatchFlag = 1(TEXT)
])
}
预字符串化(Static Stringification): 大量连续静态节点直接转为字符串,跳过虚拟 DOM 创建。
缓存事件处理函数:
// Vue2 - 每次渲染创建新函数
onClick: () => this.handleClick()
// Vue3 - 缓存函数,避免子组件不必要的更新
onClick: cache[0] || (cache[0] = () => this.handleClick())
48. PatchFlag
定义: 编译时在动态节点上标记的数字,标识节点哪些部分是动态的。
常见 PatchFlag 值:
1 (TEXT) - 动态文本节点
2 (CLASS) - 动态 class
4 (STYLE) - 动态 style
8 (PROPS) - 动态属性(非 class/style)
16 (FULL_PROPS) - 动态 key 属性
32 (HYDRATE_EVENTS) - 事件监听
64 (STABLE_FRAGMENT) - 稳定片段
原理: diff 时根据 PatchFlag 只对比动态部分,跳过静态部分。
49. v-if 与 v-for 的优先级
Vue2: v-for 优先于 v-if
Vue3: v-if 优先于 v-for
最佳实践: 永远不要在同一元素上同时使用 v-if 和 v-for。
<!-- 不推荐 -->
<li v-for="item in items" v-if="item.isActive">{{ item.name }}</li>
<!-- 推荐:用计算属性过滤 -->
<li v-for="item in activeItems" :key="item.id">{{ item.name }}</li>
<script setup>
const activeItems = computed(() => items.value.filter(i => i.isActive))
</script>
<!-- 推荐:嵌套使用 -->
<template v-for="item in items" :key="item.id">
<li v-if="item.isActive">{{ item.name }}</li>
</template>
50. v-memo
定义: Vue3.2 引入的性能优化指令,缓存子树,依赖不变时跳过渲染。
原理: 缓存 vnode 树,当依赖值不变时直接复用缓存。
示例:
<!-- 只有 item.id 变化时才重新渲染 -->
<div v-for="item in list" :key="item.id" v-memo="[item.id]">
<p>{{ item.name }}</p>
<p>{{ item.desc }}</p>
</div>
<!-- 依赖空数组 = 永远不更新 -->
<div v-memo="[]">不会更新的内容</div>
使用场景: 长列表渲染、复杂组件的条件渲染
51. Tree-shaking
定义: 打包工具移除未使用代码的优化技术。
Vue3 的支持: 将 API 导出为独立函数,打包工具可以移除未使用的 API。
// Vue2 - 全局 API,无法 Tree-shaking
Vue.nextTick()
Vue.observable()
// Vue3 - 按需导入,支持 Tree-shaking
import { nextTick, reactive } from 'vue'
// 如果不用 ref,就不会打包 ref 的代码
52. 按需加载与组件库优化
实现方式:
// 1. 手动按需导入
import { Button, Input } from 'element-plus'
app.use(Button)
app.use(Input)
// 2. 使用 unplugin-vue-components 自动导入
// vite.config.js
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default {
plugins: [
Components({
resolvers: [ElementPlusResolver()]
})
]
}
53. 虚拟 DOM 与 Diff 算法优化
Vue3 的 Diff 优化:
- 双端 Diff:首尾同时对比
- 最长递增子序列:最少移动节点
- 静态标记跳过:PatchFlag 标识动态部分
流程:
- 对比新旧 vnode 树
- 标记不同部分
- 最少操作更新 DOM
十、Pinia 与 Vue 3 生态
54. Pinia 概述
定义: Pinia 是 Vue3 官方推荐的状态管理库,替代 Vuex。
核心概念:
- Store:状态仓库
- State:响应式状态
- Getters:计算属性
- Actions:方法(支持同步/异步)
55. Pinia 相比 Vuex 的优势
| 维度 | Vuex | Pinia |
|---|---|---|
| Mutation | 需要 mutation 修改 state | 直接修改 state,无 mutation |
| TypeScript | 支持较差 | 完整的类型推导 |
| 模块化 | modules 配置复杂 | 每个 store 独立定义 |
| 体积 | 较大 | 更小(~1KB) |
| DevTools | 支持 | 完整支持 |
| SSR | 需要额外配置 | 原生支持 |
| 代码分割 | 困难 | 天然支持 |
| 学习曲线 | 较陡 | 平缓 |
56. Pinia 的使用
定义 Store:
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// 方式1:Composition API 风格
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
const increment = () => count.value++
return { count, doubleCount, increment }
})
// 方式2:Options 风格
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
},
async fetchData() {
const res = await fetch('/api/data')
this.count = await res.json()
}
}
})
使用 Store:
<script setup>
import { useCounterStore } from './stores/counter'
const counter = useCounterStore()
counter.increment()
console.log(counter.count)
console.log(counter.doubleCount)
</script>
57. Vue Router 在 Vue3 中的变化
// Vue2
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const router = new VueRouter({ routes })
// Vue3
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes
})
const app = createApp(App)
app.use(router)
十一、组件通信与高级话题
58. Vue3 组件通信方式
| 方式 | 适用场景 | 示例 |
|---|---|---|
| props / emits | 父子组件 | props.title, emit('click') |
| v-model | 父子双向绑定 | v-model="value" |
| provide / inject | 跨层级组件 | provide('key', val) |
| ref / expose | 父访问子 | childRef.value.method() |
| attrs / slots | 透传属性/内容 | v-bind="$attrs" |
| Pinia/全局状态 | 全局共享 | useStore() |
| 事件总线(mitt) | 任意组件 | emitter.emit() |
| 自定义 Hooks | 逻辑复用 | useMouse() |
59. 自定义 Hooks(Composables)
定义: 封装和复用逻辑的函数,使用 Composition API。
示例 - useMouse:
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(e) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
// 使用
<script setup>
import { useMouse } from './composables/useMouse'
const { x, y } = useMouse()
</script>
Hooks 与 Mixins 的区别:
| 维度 | Mixins | Hooks |
|---|---|---|
| 来源清晰 | 不清楚(多个 mixin 合并) | 清楚(显式导入调用) |
| 命名冲突 | 可能发生 | 不会(独立变量) |
| 类型推导 | 不支持 | 完整支持 |
| 灵活性 | 低 | 高 |
60. 响应式丢失问题与 toRefs 解决
问题场景:
function useFeature() {
const state = reactive({ count: 0, name: 'vue' })
return state // 直接返回
}
// 使用时解构会丢失响应性
const { count, name } = useFeature() // count 不是 ref
count++ // 不会触发更新
解决方案:
function useFeature() {
const state = reactive({ count: 0, name: 'vue' })
return toRefs(state) // 转换为 ref
}
// 使用时保持响应性
const { count, name } = useFeature()
count.value++ // 正常触发更新
61. 动态组件
实现: 使用 <component :is="componentName">
<script setup>
import { ref, shallowRef } from 'vue'
import Home from './Home.vue'
import About from './About.vue'
const currentTab = shallowRef(Home)
const tabs = { Home, About }
</script>
<template>
<button v-for="(_, tab) in tabs" @click="currentTab = tabs[tab]">
{{ tab }}
</button>
<component :is="currentTab" />
</template>
62. keep-alive
定义: 缓存组件实例,避免重复渲染。
<keep-alive :include="['Home', 'About']" :max="10">
<component :is="currentTab" />
</keep-alive>
属性:
include:缓存的组件名exclude:不缓存的组件名max:最大缓存数量
63. props 解构问题
问题: Vue3.3 之前,解构 props 会丢失响应性。
// Vue3.3 之前
const props = defineProps(['title'])
const { title } = props // title 不是响应式
解决方案:
// 方式1:使用 toRefs
import { toRefs } from 'vue'
const props = defineProps<{ title: string }>()
const { title } = toRefs(props)
// 方式2:Vue3.4+ 响应式解构(实验性)
const { title } = defineProps<{ title: string }>()
// 方式3:Vue3.5+ 使用 defineModel
const model = defineModel()
十二、TypeScript 支持
64. Vue3 对 TypeScript 的改进
Vue2 的问题:
- Options API 类型推导困难
- 需要装饰器(@Component)或复杂配置
- this 类型不明确
Vue3 的改进:
- 完整的 TypeScript 重写
- Composition API 天然支持 TS
- 编译器宏自动类型推导
<script setup>类型自动推断
示例:
<script setup lang="ts">
import { ref } from 'vue'
// 类型自动推导
const count = ref(0) // Ref<number>
const name = ref<string | null>(null)
// Props 类型
const props = defineProps<{
title: string
count?: number
}>()
// Emits 类型
const emit = defineEmits<{
(e: 'update', value: string): void
}>()
// 泛型组件
const items = ref<string[]>([])
</script>
十三、自定义渲染器
65. createRenderer 与自定义渲染器
定义: 创建自定义渲染器,将 Vue 组件渲染到非 DOM 环境(如 Canvas、小程序、Native)。
示例:
import { createRenderer } from 'vue'
const renderer = createRenderer({
createElement(type) {
// 创建元素
return { type, children: [] }
},
setElementText(node, text) {
// 设置文本
node.text = text
},
insert(child, parent, anchor) {
// 插入元素
parent.children.push(child)
},
// ... 其他必要方法
})
const app = renderer.createApp(App)
十四、项目迁移与创建
66. 创建 Vue3 项目
# 使用 Vite(推荐)
npm create vite@latest my-app -- --template vue
# 使用 TypeScript
npm create vite@latest my-app -- --template vue-ts
# 进入项目
cd my-app
npm install
npm run dev
67. Vue2 迁移到 Vue3
步骤:
- 安装迁移工具
npm install @vue/compat
- 配置别名
// vite.config.js
resolve: {
alias: {
vue: '@vue/compat'
}
}
- 逐步迁移
- 替换
beforeDestroy→beforeUnmount - 替换
destroyed→unmounted - 替换
Vue.use→app.use - 替换全局过滤器为 computed/方法
- 替换
.sync修饰符为v-model:prop
- 使用官方迁移构建
configureCompat({
MODE: 3, // 或 2
GLOBAL_MOUNT: false,
GLOBAL_EXTEND: false,
})
十五、响应式数据判断方法总结
68. 响应式判断 API 汇总
| API | 作用 | 返回值 |
|---|---|---|
isRef(val) | 判断是否为 ref | boolean |
isReactive(val) | 判断是否为 reactive | boolean |
isReadonly(val) | 判断是否为 readonly | boolean |
isProxy(val) | 判断是否为 Proxy | boolean |
toRaw(val) | 获取原始对象 | object |
markRaw(val) | 标记为永不转换 | object |
unref(val) | 解包 ref | value |
十六、总结速查表
Composition API 核心 API
| API | 用途 | 返回值 |
|---|---|---|
ref(value) | 基本类型响应式 | Ref<T> |
reactive(obj) | 对象响应式 | Proxy |
computed(fn) | 计算属性 | Ref<T> |
watch(src, cb) | 侦听数据 | StopHandle |
watchEffect(fn) | 自动追踪副作用 | StopHandle |
provide(key, val) | 提供依赖 | void |
inject(key) | 注入依赖 | any |
onMounted(fn) | 挂载钩子 | void |
toRefs(obj) | 转 ref 对象 | Object |
toRef(obj, key) | 转单个 ref | Ref |
生命周期钩子
| Composition API | 时机 |
|---|---|
| setup | beforeCreate/created 之前 |
| onBeforeMount | DOM 挂载前 |
| onMounted | DOM 挂载后 |
| onBeforeUpdate | DOM 更新前 |
| onUpdated | DOM 更新后 |
| onBeforeUnmount | 组件销毁前 |
| onUnmounted | 组件销毁后 |