← 返回场景决策

Vue 3 场景决策

什么时候用 / 什么时候不用 / 边界在哪里 — 8 大核心场景全解析

KeepAlive computed vs watch ref vs reactive v-if vs v-show nextTick Pinia vs Vuex 组件通信 路由守卫

1. KeepAlive — 组件缓存决策

KeepAlive = 浏览器标签页 切到别的 tab 不会关掉,回来还在
不用 KeepAlive = 每次重新打开网页 关掉再打开,一切从头来
类比理解:KeepAlive 就像浏览器标签页——你从标签 A 切到标签 B,A 并没有关闭,只是隐藏了;切回来时,A 的内容原封不动。不用 KeepAlive 就像每次都关闭网页再重新打开——所有状态归零,页面重新加载。
✅ 什么时候用

1. 列表页 → 详情页 → 返回列表页,保留滚动位置和筛选条件

用户在列表页滚到了第 3 屏,点进详情页,返回后不想重新从第 1 屏开始。KeepAlive 可以保留组件实例,回来时滚动位置、分页、搜索条件都在。

2. Tab 切换保留表单状态

多 Tab 页,用户在 Tab A 填了一半表单,切到 Tab B 查看,再切回 A 时不想表单被清空。

3. 频繁切换的组件

如后台管理的多 Tab 页签、仪表盘的不同面板、编辑器的多个文件 Tab。频繁创建/销毁组件开销大,缓存后切换更流畅。

❌ 什么时候不能用 / 边界

1. 组件内部有定时器/轮询,离开时必须停止

KeepAlive 只是"休眠"组件,不会触发 unmounted。如果组件有 setInterval 轮询,切走后定时器仍在执行,浪费资源甚至造成 bug。

2. 同一路由不同参数(如 /user/1 → /user/2)需要重新加载

默认情况下,同一路由组件被缓存后,参数变化不会重新创建组件实例。需要配合 onActivated 或 watch 路由参数来手动刷新。

3. 组件数据依赖 props 变化需要每次刷新

如果组件的渲染完全依赖父组件传入的 props,且 props 每次都不同,缓存会导致显示旧数据。

4. 内存敏感页面(大量组件缓存会 OOM)

每个缓存的组件实例都占用内存。如果页面有很多大组件(如大数据表格、图表),全缓存可能导致内存溢出。需用 max 限制缓存数量。

include / exclude / max 属性详解
📀 vue-app
📁 src/views/
KeepAlive-demo.vue
router/index.js
// include: 只有匹配的组件会被缓存(支持字符串、正则、数组) <KeepAlive include="UserList,OrderList"> <component :is="currentTab" /> </KeepAlive> // exclude: 匹配的组件不会被缓存 <KeepAlive :exclude="/(Settings|Log)/"> <component :is="currentTab" /> </KeepAlive> // max: 最多缓存多少组件实例(LRU 策略,最久未访问的先销毁) <KeepAlive :max="10"> <router-view v-slot="{ Component }"> <component :is="Component" /> </router-view> </KeepAlive> // 动态 include:结合 ref 动态控制缓存列表 <KeepAlive :include="cacheList"> <router-view v-slot="{ Component }"> <component :is="Component" /> </router-view> </KeepAlive>
面试金句 Slot(插槽) 是 Vue 组件内容分发的机制:父组件向子组件传递模板内容。三类插槽:默认插槽、具名插槽(v-slot:name / #name)、作用域插槽(子组件向父组件传数据)。Vue 3 语法:v-slot 简写为 #。使用场景:布局组件、列表组件、可定制 UI 组件库。
注意:include/exclude 匹配的是组件的 name 选项。在 <script setup> 中,组件名默认取文件名,建议用 defineOptions({ name: 'UserList' }) 显式指定。
onActivated / onDeactivated 生命周期
📀 vue-app
📁 src/views/
KeepAlive-lifecycle.vue
Comp-2.vue
import { onActivated, onDeactivated, ref, onMounted, onUnmounted } from 'vue' // 组件被激活(从缓存中恢复显示)时触发 onActivated(() => { // 适合:恢复轮询、恢复定时器、刷新数据 resumePolling() console.log('组件被激活') }) // 组件被停用(切走但不销毁)时触发 onDeactivated(() => { // 适合:暂停轮询、暂停定时器、保存临时状态 pausePolling() console.log('组件被停用') }) // 生命周期触发顺序对比: // 首次进入:mounted → activated // 切走: deactivated(不会触发 unmounted) // 切回来: activated(不会触发 mounted) // 销毁缓存:unmounted
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
完整示例:路由级 KeepAlive + meta 标记
📀 vue-app
📁 src/router/
index.js
App.vue
// router/index.js import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/', component: () => import('../views/Home.vue') }, { path: '/users', name: 'UserList', component: () => import('../views/UserList.vue'), meta: { keepAlive: true } // ✅ 需要缓存 }, { path: '/users/:id', name: 'UserDetail', component: () => import('../views/UserDetail.vue'), meta: { keepAlive: false } // ❌ 不缓存 }, { path: '/settings', name: 'Settings', component: () => import('../views/Settings.vue'), meta: { keepAlive: false } // ❌ 不缓存(每次都要刷新设置) }, { path: '/orders', name: 'OrderList', component: () => import('../views/OrderList.vue'), meta: { keepAlive: true } // ✅ 需要缓存 } ] const router = createRouter({ history: createWebHistory(), routes }) export default router
面试金句 KeepAlive 缓存不活动的组件实例,避免重复渲染。核心属性:include/exclude(白名单/黑名单组件名)、max(最大缓存数)。生命周期:onActivated(从缓存恢复)和 onDeactivated(被缓存时)。常见场景:Tab 切换、列表→详情返回时保留滚动位置和表单状态。
📀 vue-app
📁 src/
App.vue
router/index.js
<!-- App.vue --> <template> <div> <nav> <router-link to="/">首页</router-link> <router-link to="/users">用户列表</router-link> <router-link to="/orders">订单列表</router-link> <router-link to="/settings">设置</router-link> </nav> <!-- 核心:根据路由 meta.keepAlive 决定是否缓存 --> <router-view v-slot="{ Component, route }"> <KeepAlive :include="cacheList"> <component :is="Component" :key="route.path" /> </KeepAlive> </router-view> </div> </template> <script setup> import { ref, watch } from 'vue' import { useRoute } from 'vue-router' const route = useRoute() const cacheList = ref(['UserList', 'OrderList']) // 动态管理缓存列表:根据 meta.keepAlive 添加/移除 watch( () => route.name, (name) => { if (route.meta.keepAlive && name && !cacheList.value.includes(name)) { cacheList.value.push(name) } }, { immediate: true } ) </script>
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
include 动态控制:移除缓存
📀 vue-app
📁 src/views/
UserList.vue
router/index.js
// 在组件内部主动将自己从缓存列表移除 // UserList.vue <script setup> import { onDeactivated } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useCacheStore } from '@/stores/cache' const route = useRoute() const router = useRouter() const cacheStore = useCacheStore() // 某些条件下主动清除自己的缓存 function invalidateCache() { cacheStore.removeFromCache('UserList') } // 示例:数据发生重大变更后,下次进入需要刷新 async function handleBulkDelete() { await deleteUsers(selectedIds) invalidateCache() // 下次进入时重新加载 fetchUsers() // 当前也需要刷新 } </script>
面试金句 KeepAlive 的 include/exclude 通过组件 name 匹配来精确控制缓存范围。动态修改 include 数组可以主动移除缓存——当组件 name 从数组中删除时,该组件的缓存实例会被立即销毁。适合"数据变更后需要刷新"的场景。
路由参数变化的处理
📀 vue-app
📁 src/views/
UserDetail.vue
router/index.js
// 同一组件、不同参数:/user/1 → /user/2 // 方案 1:用 :key 强制重建(最简单,但失去缓存意义) <component :is="Component" :key="route.fullPath" /> // 方案 2:watch 路由参数,手动刷新数据(保留缓存但更新数据) <script setup> import { watch, onActivated } from 'vue' import { useRoute } from 'vue-router' const route = useRoute() const userData = ref(null) async function loadUser(id) { userData.value = await fetchUser(id) } // 首次挂载和参数变化时加载 watch( () => route.params.id, (newId) => { if (newId) loadUser(newId) }, { immediate: true } ) // 激活时也可以刷新(从详情页返回时) onActivated(() => { if (route.params.id) { loadUser(route.params.id) } }) </script>
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
轮询组件的 KeepAlive 处理
📀 vue-app
📁 src/views/
Dashboard.vue
Comp-7.vue
// Dashboard.vue — 有实时轮询的组件 <script setup> import { ref, onActivated, onDeactivated } from 'vue' const metrics = ref({}) let pollingTimer = null function startPolling() { fetchMetrics() // 立即获取一次 pollingTimer = setInterval(fetchMetrics, 5000) } function stopPolling() { if (pollingTimer) { clearInterval(pollingTimer) pollingTimer = null } } async function fetchMetrics() { metrics.value = await api.getMetrics() } // ✅ 激活时启动轮询 onActivated(() => { startPolling() }) // ✅ 停用时停止轮询(关键!否则切走后还在请求) onDeactivated(() => { stopPolling() }) </script>
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
决策表
场景用不用原因
列表页 → 详情页 → 返回✅ 用保留滚动位置、筛选条件、分页状态
多 Tab 切换保留表单✅ 用避免用户填写的内容丢失
频繁切换的仪表盘面板✅ 用减少重复创建/销毁的开销
组件有定时器/轮询⚠️ 慎用必须配合 onDeactivated 清理,否则轮询不会停止
同路由不同参数(/user/1→2)⚠️ 需处理缓存导致参数变化不重载,需 watch 路由参数或用 :key
每次进入都要最新数据❌ 不用缓存会导致数据过时,需要在 onActivated 中手动刷新
大量组件都缓存❌ 不用内存压力,用 max 限制或选择性缓存
单次展示页(如结果页)❌ 不用用完即走,无需缓存
警告:内存泄漏风险
KeepAlive 缓存的组件不会被销毁,其中的事件监听器、定时器、WebSocket 连接都不会自动清理。必须在 onDeactivated 中手动清理。否则即使组件不可见,这些资源仍在运行,导致内存泄漏和性能问题。
警告:路由参数问题
同一个路由组件被缓存后,从 /user/1 导航到 /user/2 时,组件不会重新创建。必须通过 watch(route.params) 或 :key="route.fullPath" 来处理。用 :key 会失去缓存效果,watch 更灵活但代码更多。

2. computed vs watch — 响应式选型

computed = Excel 公式 A1 = B1 + C1,改了 B1 自动变,永远是"计算结果"
watch = 宏/VBA 改了 A1 → 触发一段操作(发请求、改 DOM、写数据库)
类比理解:computed 就像 Excel 中的公式单元格——A1=B1+C1,你改了 B1,A1 自动更新,它永远是"计算结果"本身。watch 就像 Excel 的宏——当某个单元格变化时,触发一段操作(发请求、写文件、改其他单元格),它关注的是"变化后做什么事"。
✅ computed 什么时候用

1. 派生状态 — 从现有数据计算出新数据

如 fullName = firstName + lastName、total = items.reduce(...)、filtered = list.filter(...)。核心:结果完全由依赖决定,无副作用。

2. 过滤 / 排序

搜索过滤、按日期排序、按状态分组。输入变 → 输出自动变。

3. 格式化

日期格式化、金额格式化、百分比计算。原始值不变,展示格式随原始值自动更新。

4. 多个源合并一个结果

从多个 ref/reactive 组合出一个结果,如 isReady = loaded && authenticated && !error。

📀 vue-app
📁 src/views/
computed-demo.vue
Comp-8.vue
// computed 典型用法 <script setup> import { ref, computed } from 'vue' // 1. 派生状态 const firstName = ref('张') const lastName = ref('三') const fullName = computed(() => firstName.value + lastName.value) // 2. 过滤 + 排序 const searchQuery = ref('') const users = ref([ { name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }, { name: 'Charlie', age: 35 } ]) const filteredUsers = computed(() => { const q = searchQuery.value.toLowerCase() return users.value .filter(u => u.name.toLowerCase().includes(q)) .sort((a, b) => a.age - b.age) }) // 3. 格式化 const price = ref(1299.5) const formattedPrice = computed(() => { return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(price.value) }) // formattedPrice.value → "¥1,299.50" // 4. 多源合并 const loaded = ref(false) const authenticated = ref(false) const hasError = ref(false) const isReady = computed(() => loaded.value && authenticated.value && !hasError.value) </script>
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
✅ watch 什么时候用

1. 异步操作(API 调用)

数据变化后发请求、查询后端。computed 不能异步。

2. 副作用(DOM 操作 / 路由跳转 / LocalStorage)

修改 DOM、写 localStorage、跳转路由、操作第三方库实例。这些都不是"计算结果"。

3. 开销大的操作(防抖搜索)

搜索输入防抖后发请求,不能用 computed(每次击键都重新计算)。

4. 需要旧值对比

如:状态从 A 变到 B 时做特定处理,需要知道旧值和新值。

📀 vue-app
📁 src/views/
watch-demo.vue
Comp-9.vue
// watch 典型用法 <script setup> import { ref, watch } from 'vue' import { useRouter } from 'vue-router' // 1. 异步操作:搜索防抖 const searchQuery = ref('') const results = ref([]) const loading = ref(false) let debounceTimer = null watch(searchQuery, (newVal) => { clearTimeout(debounceTimer) debounceTimer = setTimeout(async () => { loading.value = true try { results.value = await api.search(newVal) } finally { loading.value = false } }, 300) }) // 2. 副作用:自动保存到 localStorage const draft = ref('') watch(draft, (newVal) => { localStorage.setItem('draft', newVal) }) // 3. 需要旧值:状态变化检测 const status = ref('idle') watch(status, (newVal, oldVal) => { if (oldVal === 'idle' && newVal === 'running') { showStartNotification() } if (newVal === 'error') { reportError({ from: oldVal, to: newVal }) } }) // 4. 路由参数变化时重新请求 const route = useRoute() const userData = ref(null) watch( () => route.params.id, async (newId) => { if (newId) { userData.value = await api.getUser(newId) } }, { immediate: true } ) </script>
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
❌ computed 不能做什么
📀 vue-app
📁 src/views/
computed-pitfall.vue
Comp-10.vue
// ❌ 错误:computed 中做异步操作 const data = computed(async () => { const res = await fetch('/api/data') // ❌ computed 不支持异步 return res.json() // 返回的是 Promise,不是值 }) // ❌ 错误:computed 中修改外部状态 const count = ref(0) const doubled = computed(() => { count.value++ // ❌ 不能在 computed 中修改其他响应式数据 return count.value * 2 }) // ❌ 错误:computed 没有返回值 const logger = computed(() => { console.log(count.value) // ❌ 只是为了执行副作用,不是计算 // 没有 return })
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
❌ watch 不该做什么
📀 vue-app
📁 src/views/
watch-pitfall.vue
Comp-11.vue
// ❌ 不该:纯计算(应该用 computed) const firstName = ref('张') const lastName = ref('三') const fullName = ref('') // ❌ 错误做法:用 watch 赋值 watch([firstName, lastName], ([f, l]) => { fullName.value = f + l // 应该用 computed }) // ✅ 正确做法 const fullName = computed(() => firstName.value + lastName.value) // ❌ 不该:只为了赋值给另一个变量 const price = ref(100) const discountPrice = ref(0) // ❌ 错误做法 watch(price, (newVal) => { discountPrice.value = newVal * 0.8 // 应该用 computed }) // ✅ 正确做法 const discountPrice = computed(() => price.value * 0.8)
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
watchEffect vs watch 区别
📀 vue-app
📁 src/views/
watchEffect-demo.vue
Comp-12.vue
// watchEffect: 自动追踪依赖,立即执行,拿不到旧值 import { ref, watchEffect } from 'vue' const userId = ref(1) const data = ref(null) // 自动追踪内部使用的所有响应式数据 const stop = watchEffect(async () => { // 自动追踪 userId.value data.value = await api.getUser(userId.value) }) stop() // 手动停止 // watch: 需要显式指定监听源,可拿到新值和旧值 watch( userId, // 显式指定监听什么 async (newId, oldId) => { // 可以拿到新值和旧值 console.log(`从 ${oldId} 变为 ${newId}`) data.value = await api.getUser(newId) } ) // watchEffect 适用场景: // - 不需要旧值 // - 想立即执行 // - 依赖关系复杂,懒得列出 // watch 适用场景: // - 需要新旧值对比 // - 需要精确控制监听哪个数据 // - 需要延迟执行(不设 immediate) // - 需要深层监听(deep: true)
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
特性computedwatchwatchEffect
必须有返回值✅ 是❌ 否❌ 否
支持异步❌ 否✅ 是✅ 是
可以产生副作用❌ 否✅ 是✅ 是
缓存结果✅ 是(依赖不变不重算)❌ 否❌ 否
获取旧值❌ 否✅ 是❌ 否
自动追踪依赖✅ 是❌ 需显式指定✅ 是
默认立即执行✅ 是❌ 否(需 immediate)✅ 是
典型场景派生/过滤/格式化API/副作用/旧值对比立即执行副作用
决策表
场景用哪个原因
全名 = 姓 + 名computed纯计算,必须有返回值
搜索关键词变化 → 发 APIwatch异步操作 + 副作用
过滤列表computed纯函数,缓存结果
数据变化 → 写 localStoragewatch副作用
金额格式化computed纯转换
路由参数变 → 请求新数据watch异步请求
状态从 A 变到 B → 特殊处理watch需要旧值
自动保存草稿watch / watchEffect副作用,不需要旧值可用 watchEffect
多条件判断是否可提交computed纯逻辑判断
警告:watch 的 immediate 陷阱
watch 默认不立即执行(lazy),只在监听源变化时触发。如果加 immediate: true,回调会立即执行一次,此时旧值为 undefined。需要处理 oldVal 为 undefined 的情况,否则可能报错。
警告:watch 的 deep 陷阱
deep: true 会深度遍历对象的所有嵌套属性,开销很大。对大对象使用 deep 监听可能导致性能问题。替代方案:精确监听某个嵌套属性 () => obj.nested.field,或使用 watchEffect 让它自动追踪。
📀 vue-app
📁 src/views/
deep-watch.vue
Comp-13.vue
// deep 的正确用法和替代方案 // ❌ 开销大:深度监听整个大对象 watch( bigObject, (newVal) => { /* ... */ }, { deep: true } // 遍历所有嵌套属性,性能差 ) // ✅ 精确监听某个嵌套属性 watch( () => bigObject.value.config.theme, // 只监听这个路径 (newTheme) => { applyTheme(newTheme) } ) // ✅ 或者用 watchEffect 自动追踪 watchEffect(() => { // 只追踪实际访问的属性 applyTheme(bigObject.value.config.theme) }) // immediate 的正确处理 watch( userId, async (newId, oldId) => { // immediate 时 oldId 是 undefined if (oldId !== undefined && newId === oldId) return userData.value = await api.getUser(newId) }, { immediate: true } )
面试金句 watchEffect 自动追踪回调函数内部的响应式依赖,无需手动声明。vs watch:watchEffect 立即执行(类似 React useEffect 无依赖数组)、无法获取旧值、适合执行副作用而非响应数据变化。清理副作用:onCleanup 回调或返回的 stop 函数。

3. ref vs reactive — 响应式数据选型

ref = 保鲜盒 把东西装进去,拿出来时要打开 .value 盖子
reactive = 整个冰箱 直接往里面放东西,但不能把冰箱搬走换一个
类比理解:ref 就像保鲜盒——你把食物(值)装进保鲜盒里,每次取用都得打开盖子(.value)。好处是可以随时换一盒新的东西。reactive 就像整个冰箱——你直接往里面放东西,不需要开盖子,但冰箱不能整个搬走换一台(不能重新赋值整个对象,否则丢失响应式)。
✅ ref 什么时候用

1. 单个值(数字 / 字符串 / 布尔)

reactive 不能用于基本类型,ref 是唯一选择。

2. 从函数返回响应式数据

composable 函数返回 ref,调用方可以随意重新赋值而不丢失响应式。

3. 需要重新赋值整个变量

如从 API 获取数据后整体替换、切换选中项、重置状态。ref.value = newObj 可以整体替换,reactive 做不到。

✅ reactive 什么时候用

1. 对象 / 数组

当数据本身就是对象结构,访问时不需要 .value,代码更简洁。

2. 表单数据

表单有多个字段,用 reactive 组织更直观,访问 form.name 而不是 form.value.name。

3. 不需要重新赋值引用的复杂结构

如果对象的引用不会变(只会修改属性),reactive 更方便。

❌ reactive 的坑
📀 vue-app
📁 src/views/
reactive-pitfalls.vue
Comp-14.vue
// ❌ 坑1:重新赋值整个对象 → 丢失响应式 const state = reactive({ name: 'Alice', age: 30 }) // ❌ 错误:整体替换丢失响应式 state = { name: 'Bob', age: 25 } // ❌ TypeError: Assignment to constant variable // 即使不用 const,用 let,新对象也不是响应式的 // ✅ 正确:逐个属性赋值 state.name = 'Bob' state.age = 25 // ✅ 或者用 Object.assign Object.assign(state, { name: 'Bob', age: 25 }) // ❌ 坑2:从函数返回后解构 → 丢失响应式 const form = reactive({ name: '', email: '' }) const { name, email } = form // ❌ 解构后 name 和 email 是普通变量,不是响应式的 // ✅ 正确:用 toRefs import { toRefs } from 'vue' const { name, email } = toRefs(form) // ✅ name 和 email 是 ref,保持响应式 // ❌ 坑3:用在基本类型 const count = reactive(0) // ❌ reactive 不支持基本类型,会报警告 // ✅ 正确:基本类型只能用 ref const count = ref(0)
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
toRefs 解决解构丢失响应式
📀 vue-app
📁 src/views/
toRefs-demo.vue
Comp-15.vue
import { reactive, toRefs } from 'vue' // composable 返回 reactive + toRefs function useUserForm() { const form = reactive({ name: '', email: '', phone: '' }) function reset() { Object.assign(form, { name: '', email: '', phone: '' }) } function submit() { return api.saveUser({ ...form }) } // ✅ 用 toRefs 包装,解构后仍然是响应式的 return { ...toRefs(form), reset, submit } } // 使用 const { name, email, phone, reset, submit } = useUserForm() // name, email, phone 都是 ref,保持响应式
面试金句 Composable(组合式函数) 是 Vue 3 逻辑复用的核心模式:以 use 开头的函数,封装响应式状态和逻辑。vs Mixins:Composable 有明确的数据来源、无命名冲突、TypeScript 类型安全。设计原则:每个 composable 只负责一个功能(如 useMouse、useFetch、useLocalStorage),返回 ref 和方法。
ref vs reactive 全面对比
特性refreactive
支持基本类型✅ 是❌ 否
支持对象类型✅ 是✅ 是
访问方式.value(模板自动解包)直接访问属性
可以整体替换✅ ref.value = newObj❌ 丢失响应式
解构后保持响应式✅ 是❌ 需 toRefs
从函数返回✅ 安全⚠️ 需注意解构
模板中使用自动解包,无需 .value直接使用
在 reactive 中使用自动解包直接嵌套
TS 类型推导Ref<T>T(更自然)
决策表
场景用哪个原因
计数器、开关、IDref基本类型,reactive 不支持
从 API 获取的数据(整体替换)refdata.value = newData 可以整体替换
composable 函数返回值ref调用方可以随意重新赋值
表单数据reactive多字段对象,访问更自然
配置对象(不改引用)reactive只改属性不改引用
数组(push/splice 操作)reactive操作更自然,不需要 .value
不确定用什么refref 更安全,通用性更好
综合示例
📀 vue-app
📁 src/views/
ref-vs-reactive.vue
Comp-16.vue
// 场景 1:列表数据 + 筛选(ref 为主) <script setup> import { ref, computed } from 'vue' const users = ref([]) // ✅ ref:API 返回后整体替换 const loading = ref(false) // ✅ ref:基本类型 const searchQuery = ref('') // ✅ ref:基本类型 const currentPage = ref(1) // ✅ ref:基本类型 async function fetchUsers() { loading.value = true try { // ✅ 可以整体替换 users.value = await api.getUsers({ page: currentPage.value, query: searchQuery.value }) } finally { loading.value = false } } </script> // 场景 2:表单数据(reactive 更自然) <script setup> import { reactive, toRefs } from 'vue' const form = reactive({ username: '', email: '', password: '', confirmPassword: '', agree: false }) function handleSubmit() { // 直接访问属性,无需 .value if (form.password !== form.confirmPassword) { return showError('密码不一致') } api.register(form) } function handleReset() { // ✅ Object.assign 保持响应式 Object.assign(form, { username: '', email: '', password: '', confirmPassword: '', agree: false }) } </script> // 场景 3:混用(推荐模式) <script setup> import { ref, reactive } from 'vue' // 基本类型用 ref const count = ref(0) const isActive = ref(false) const selectedId = ref(null) // 固定结构的对象用 reactive const pagination = reactive({ page: 1, pageSize: 20, total: 0 }) // 需要整体替换的用 ref const userList = ref([]) const currentUser = ref(null) </script>
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
警告:解构丢失响应式
reactive 对象解构后,每个属性变成普通变量,不再有响应式。必须用 toRefs() 包装后再解构。这是 reactive 最常见的坑。
警告:重新赋值问题
reactive 不能重新赋值整个对象。如果需要整体替换数据,用 ref 或 Object.assign()。在 composable 中推荐返回 ref,因为调用方可能需要重新赋值。

4. v-if vs v-show — 条件渲染决策

v-if = 临时工 需要时招来,不需要时辞退(创建/销毁 DOM)
v-show = 常驻员工 一直在,忙时上班闲时休息(display: none/block)
类比理解:v-if 就像临时工——忙的时候招进来(创建 DOM),不忙的时候辞退(销毁 DOM),每次招人都有成本(创建开销)。v-show 就像常驻员工——一直在公司(DOM 一直存在),忙的时候上班(display: block),闲的时候休息(display: none),切换成本低但一直占着工位(内存)。
✅ v-if 什么时候用

1. 条件很少变化

如权限判断、角色判断——登录后基本不变,用 v-if 避免渲染不需要的 DOM。

2. 初始条件为 false(不渲染)

如弹窗、抽屉——默认不显示,用 v-if 避免初始化开销(组件不会 mount,不会发请求)。

3. 需要配合 transition

Vue 的 <transition> 组件的 enter/leave 动画需要 v-if 的真实 DOM 创建/销毁来触发。

✅ v-show 什么时候用

1. 频繁切换(如 Tab)

Tab 切换、折叠面板、工具栏显隐。v-show 只切换 CSS display 属性,开销极低。

2. 初始化就需要 DOM(如获取元素尺寸)

需要获取元素的 offsetWidth/offsetHeight,v-if 为 false 时元素不存在,无法获取。

3. 条件变化极频繁

如鼠标悬停显示 tooltip、拖拽时显示辅助线。v-if 反复创建/销毁开销大。

❌ 不该用的情况
📀 vue-app
📁 src/views/
v-if-pitfall.vue
Comp-17.vue
// ❌ v-if 不该用于频繁切换 <template> <!-- ❌ 错误:Tab 切换用 v-if,每次切换都创建/销毁组件 --> <div v-if="activeTab === 'home'">首页内容</div> <div v-if="activeTab === 'profile'">个人资料</div> <div v-if="activeTab === 'settings'">设置</div> <!-- ✅ 正确:Tab 切换用 v-show --> <div v-show="activeTab === 'home'">首页内容</div> <div v-show="activeTab === 'profile'">个人资料</div> <div v-show="activeTab === 'settings'">设置</div> </template> // ❌ v-show 不该用于条件为 false 时也要初始化的场景 <template> <!-- ❌ 错误:弹窗用 v-show,即使不显示也会 mount 组件发请求 --> <DetailModal v-show="showModal" :id="currentId" /> <!-- ✅ 正确:弹窗用 v-if,不显示时不会创建组件 --> <DetailModal v-if="showModal" :id="currentId" /> </template>
面试金句 v-if vs v-show:v-if 是真正的条件渲染(false 时 DOM 不存在),v-show 只是 CSS display:none(DOM 始终存在)。选择原则:频繁切换用 v-show(避免反复创建/销毁 DOM),条件不常变用 v-if(减少初始渲染开销)。v-if 和 v-for 不要同时用在同一元素
Tab 切换示例
📀 vue-app
📁 src/views/
Tab-switch.vue
Comp-18.vue
<template> <div class="tab-container"> <div class="tab-header"> <button v-for="tab in tabs" :key="tab.key" :class="['tab-btn', { active: activeTab === tab.key }]" @click="activeTab = tab.key" > {{ tab.label }} </button> </div> <!-- ✅ 频繁切换用 v-show --> <div v-show="activeTab === 'overview'" class="tab-content"> <OverviewPanel /> </div> <div v-show="activeTab === 'analytics'" class="tab-content"> <AnalyticsPanel /> </div> <div v-show="activeTab === 'settings'" class="tab-content"> <SettingsPanel /> </div> </div> </template> <script setup> import { ref } from 'vue' import OverviewPanel from './OverviewPanel.vue' import AnalyticsPanel from './AnalyticsPanel.vue' import SettingsPanel from './SettingsPanel.vue' const activeTab = ref('overview') const tabs = [ { key: 'overview', label: '概览' }, { key: 'analytics', label: '数据分析' }, { key: 'settings', label: '设置' } ] </script>
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
权限控制示例
📀 vue-app
📁 src/views/
Permission-control.vue
Comp-19.vue
<template> <!-- ✅ 权限判断用 v-if:条件几乎不变,不需要隐藏的 DOM --> <AdminPanel v-if="userStore.isAdmin" /> <UserPanel v-else /> <!-- ✅ 角色判断用 v-if --> <button v-if="hasPermission('article:delete')" @click="handleDelete"> 删除 </button> <!-- ❌ 错误:权限用 v-show,DOM 中仍然可以看到隐藏内容 --> <AdminPanel v-show="userStore.isAdmin" /> <!-- 用户可以在 DevTools 中改 display 看到 AdminPanel! --> </template> <script setup> import { useUserStore } from '@/stores/user' const userStore = useUserStore() function hasPermission(perm) { return userStore.permissions.includes(perm) } </script>
面试金句 v-if vs v-show:v-if 是真正的条件渲染(false 时 DOM 不存在),v-show 只是 CSS display:none(DOM 始终存在)。选择原则:频繁切换用 v-show(避免反复创建/销毁 DOM),条件不常变用 v-if(减少初始渲染开销)。v-if 和 v-for 不要同时用在同一元素
动画场景示例
📀 vue-app
📁 src/views/
v-if-animation.vue
Comp-20.vue
<template> <!-- ✅ v-if + transition:进入/离开动画 --> <Transition name="fade"> <Modal v-if="showModal" @close="showModal = false" /> </Transition> <!-- ✅ v-if + transition + mode:切换动画 --> <Transition name="slide" mode="out-in"> <component :is="currentView" :key="currentView" /> </Transition> </template> <style> .fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; } .fade-enter-from, .fade-leave-to { opacity: 0; } .slide-enter-active, .slide-leave-active { transition: transform 0.3s ease; } .slide-enter-from { transform: translateX(30px); } .slide-leave-to { transform: translateX(-30px); } </style>
面试金句 Transition / TransitionGroup 是 Vue 内置的过渡动画组件。Transition 处理单元素/组件的进入/离开动画(v-if/v-show),TransitionGroup 处理列表动画(v-for)。六个 CSS 类名:v-enter-from / v-enter-active / v-enter-to / v-leave-from / v-leave-active / v-leave-to。JavaScript Hooks:@before-enter、@enter、@leave 等。
v-if + template(不生成多余 DOM)
📀 vue-app
📁 src/views/
v-if-template.vue
Comp-21.vue
// 多个元素需要同一种条件判断,但不想加额外 DOM 节点 <template v-if="isLoggedIn"> <h2>欢迎回来</h2> <p>上次登录:{{ lastLogin }}</p> <button @click="handleLogout">退出</button> </template> // v-show 不支持 template(因为它操作 CSS,template 不生成 DOM 节点)
面试金句 v-if vs v-show:v-if 是真正的条件渲染(false 时 DOM 不存在),v-show 只是 CSS display:none(DOM 始终存在)。选择原则:频繁切换用 v-show(避免反复创建/销毁 DOM),条件不常变用 v-if(减少初始渲染开销)。v-if 和 v-for 不要同时用在同一元素
决策表
场景用哪个原因
Tab 切换v-show频繁切换,CSS 切换开销小
权限/角色判断v-if条件很少变,不需要隐藏 DOM
弹窗/抽屉/对话框v-if初始为 false,不渲染避免初始化开销
折叠面板v-show频繁展开/收起
tooltip / hover 提示v-show极频繁切换
需要进入/离开动画v-if配合 Transition 组件
获取隐藏元素尺寸v-showv-if 为 false 时元素不存在
条件为 false 时避免组件初始化v-if不创建组件,不发请求
大量条件渲染(性能优先)v-if减少 DOM 节点数量
警告:v-if 和 v-for 不能一起用!
Vue 3 中 v-if 优先级高于 v-for,但混用会导致逻辑混乱和性能问题。应该用 computed 先过滤,或者用 <template v-for> 包裹 <div v-if>。
📀 vue-app
📁 src/views/
v-if-v-for.vue
Comp-22.vue
// ❌ 错误:v-if 和 v-for 一起用 <div v-for="item in items" v-if="item.active"> {{ item.name }} </div> // ✅ 正确:用 computed 过滤 <div v-for="item in activeItems" :key="item.id"> {{ item.name }} </div> // computed const activeItems = computed(() => items.value.filter(item => item.active)) // ✅ 或者:外层 template v-for,内层 v-if <template v-for="item in items" :key="item.id"> <div v-if="item.active">{{ item.name }}</div> </template>
面试金句 computed 创建计算属性,自动追踪依赖并缓存结果——依赖不变时返回缓存值,依赖变化时重新计算。vs methods:methods 每次调用都执行,computed 有缓存。Vue 3 中 computed 返回只读 ref,可通过 computed({ get, set }) 创建可写计算属性。

5. nextTick — 什么时候必须等

nextTick = 等快递到货再签收 你下了单,不能马上拿到,等送到了再操作
不用 nextTick = 下单就冲去取 还没送到呢,白跑一趟
类比理解:nextTick 就像等快递——你下了单(修改数据),不能马上去取(操作 DOM),因为快递还在路上(Vue 还没更新 DOM)。nextTick 就是"等快递到了通知我",然后你再签收(操作更新后的 DOM)。不用 nextTick 就像下单后立刻冲去快递站,发现还没到,白跑一趟。
✅ 什么时候必须用

1. 修改数据后立即操作 DOM(获取高度/宽度)

Vue 的 DOM 更新是异步的,修改数据后 DOM 还没更新,此时获取的尺寸是旧的。必须等 nextTick 后再操作。

2. 修改数据后 focus 输入框

用 v-if 控制输入框的显示,v-if 变为 true 后输入框还不存在,必须在 nextTick 中 focus。

3. $refs 操作

v-for 动态渲染的元素,渲染后才能通过 $refs 访问。必须在 nextTick 中操作。

4. 动态组件加载后操作

component :is 动态切换组件后,新组件还没 mount,需要在 nextTick 后操作。

❌ 什么时候不需要

1. 在 watch 里操作 DOM

watch 回调执行时,Vue 已经完成了 DOM 更新(watch 在微任务后执行),可以直接操作 DOM。

2. 在 mounted 里

mounted 时 DOM 已经就绪,不需要 nextTick(除非 mounted 中修改了数据又想立即操作更新后的 DOM)。

3. 可以用 CSS 解决的

如果只是控制显隐、过渡动画等,CSS 就够了,不需要用 JS 操作 DOM。

nextTick 原理:微任务队列
📀 vue-app
📁 src/views/
nextTick-principle.vue
Comp-23.vue
// Vue 的更新机制: // 1. 数据变化 → 2. 标记组件需要更新 → 3. 放入微任务队列 // 4. 当前同步代码执行完 → 5. 执行微任务(更新 DOM) // 6. nextTick 回调在 DOM 更新之后执行 // 简化的原理: const pendingUpdates = [] let isFlushing = false function queueUpdate(component) { if (!pendingUpdates.includes(component)) { pendingUpdates.push(component) } if (!isFlushing) { isFlushing = true queueMicrotask(() => { // 微任务 flushUpdates() // 批量更新 DOM runNextTickCallbacks() // 执行 nextTick 回调 isFlushing = false }) } }
面试金句 nextTick 在下次 DOM 更新循环结束后执行回调。原理:Vue 的响应式更新是异步的(微任务队列),修改响应式数据后 DOM 不会立即更新,nextTick 确保在 DOM 更新后操作。使用场景:修改数据后立即获取更新后的 DOM 尺寸/位置、操作 DOM 后触发动画。
示例 1:获取 DOM 高度
📀 vue-app
📁 src/views/
get-dom-height.vue
Comp-24.vue
<script setup> import { ref, nextTick } from 'vue' const items = ref(['A', 'B', 'C']) const listRef = ref(null) const listHeight = ref(0) async function addItem() { items.value.push('D') // ❌ 错误:此时 DOM 还没更新,拿到的是旧高度 // listHeight.value = listRef.value.offsetHeight // ✅ 正确:等 DOM 更新后再获取 await nextTick() listHeight.value = listRef.value.offsetHeight console.log('列表高度:', listHeight.value) } </script> <template> <div> <ul ref="listRef"> <li v-for="item in items" :key="item">{{ item }}</li> </ul> <p>列表高度:{{ listHeight }}px</p> <button @click="addItem">添加项目</button> </div> </template>
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
示例 2:动态显示输入框并 focus
📀 vue-app
📁 src/views/
dynamic-focus.vue
Comp-25.vue
<script setup> import { ref, nextTick } from 'vue' const showInput = ref(false) const inputRef = ref(null) async function enableEdit() { showInput.value = true // v-if 变为 true,但 DOM 还没渲染 input // ❌ inputRef.value.focus() → 报错:input 还不存在 // ✅ 等 DOM 更新后再 focus await nextTick() inputRef.value.focus() } function handleBlur() { showInput.value = false } </script> <template> <div> <span v-if="!showInput" @click="enableEdit"> 点击编辑 </span> <input v-else ref="inputRef" @blur="handleBlur" /> </div> </template>
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
示例 3:聊天窗口滚动到底部
📀 vue-app
📁 src/views/
chat-scroll.vue
Comp-26.vue
<script setup> import { ref, nextTick } from 'vue' const messages = ref([]) const chatRef = ref(null) const newMessage = ref('') async function sendMessage() { if (!newMessage.value.trim()) return messages.value.push({ id: Date.now(), text: newMessage.value, isMine: true }) newMessage.value = '' // ✅ 等新消息渲染后再滚动 await nextTick() chatRef.value.scrollTop = chatRef.value.scrollHeight } // 接收他人消息也要滚动 watch(messages, async () => { await nextTick() chatRef.value.scrollTop = chatRef.value.scrollHeight }) </script> <template> <div class="chat"> <div ref="chatRef" class="chat-messages"> <div v-for="msg in messages" :key="msg.id" :class="['msg', { mine: msg.isMine }]" > {{ msg.text }} </div> </div> <input v-model="newMessage" @keyup.enter="sendMessage" /> </div> </template>
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
决策表
场景要不要 nextTick原因
修改数据后获取 DOM 尺寸✅ 必须DOM 还没更新,获取的是旧值
v-if 显示后 focus 输入框✅ 必须元素还不存在,无法 focus
渲染后滚动到指定位置✅ 必须元素还没渲染,无法滚动
动态组件后操作子组件方法✅ 必须子组件还没 mount
在 watch 中操作 DOM通常不需要watch 回调时 DOM 已更新
在 mounted 中操作 DOM通常不需要mounted 时 DOM 已就绪
可以用 CSS 替代的操作❌ 不要用CSS 更高效,优先用 CSS
提示:不要过度使用 nextTick
nextTick 是"逃生舱",不是常规操作。如果大量使用 nextTick,说明你的数据流设计有问题。优先用 Vue 的声明式渲染(computed、v-bind、watch),只在必须操作 DOM 时才用 nextTick。

6. Pinia vs Vuex — 状态管理选型

不需要状态管理 = 两三人小组口头沟通
Pinia = 企业微信(Vue3 版) 轻量、现代、类型安全
Vuex = 老式 OA 系统 能用,但笨重、模板多、TS 支持差
类比理解:如果项目只有两三个组件,就像两三人的小组——口头沟通(props/emits)就够了,不需要什么系统。Pinia 就像企业微信——轻量、现代、沟通方便,Vue3 时代的标配。Vuex 就像老式 OA 系统——功能是全的,但流程繁琐(mutations/actions/state/getters 四件套),TypeScript 支持差,新项目不推荐。
✅ Pinia 什么时候用

1. Vue 3 项目

Pinia 是 Vue 3 官方推荐的状态管理库,完美支持 Composition API。

2. 新项目

没有历史包袱,直接用 Pinia,更简洁、类型推导更好。

3. 需要 TypeScript 支持

Pinia 天然支持 TypeScript,类型推导自动,不需要手动声明类型。

4. 模块化更好

每个 Store 独立定义,不需要嵌套模块,天然 tree-shakable。

✅ Vuex 什么时候用

1. Vue 2 项目

Vue 2 + Vuex 4 是经典搭配,迁移成本高的话就别换。

2. 历史遗留项目

已有大量 Vuex 代码的项目,渐进迁移可以,不建议一次性全换。

3. 团队已经熟悉 Vuex

团队全员熟悉 Vuex,学习成本是考虑因素,但新项目仍推荐 Pinia。

❌ 什么时候不需要状态管理

1. 组件自己的状态(props/emits 够用)

只有父子组件通信,不需要全局状态。

2. 临时状态

弹窗开关、loading 状态、表单临时数据——这些放在组件内部就好。

3. 简单的跨组件通信

provide/inject 或 mitt 事件总线就够了,不需要引入整个状态管理库。

Pinia: Setup Store vs Option Store
📀 vue-app
📁 src/stores/
cart.js
user.js
// Option Store(类似 Vuex 的结构) import { defineStore } from 'pinia' export const useUserStore = defineStore('user', { state: () => ({ user: null, token: '', loading: false }), getters: { isLoggedIn: (state) => !!state.token, fullName: (state) => state.user ? `${state.user.firstName} ${state.user.lastName}` : '' }, actions: { async login(credentials) { this.loading = true try { const res = await api.login(credentials) this.token = res.token this.user = res.user localStorage.setItem('token', res.token) } finally { this.loading = false } }, logout() { this.token = '' this.user = null localStorage.removeItem('token') } } }) // Setup Store(类似 Composition API) export const useUserStore = defineStore('user', () => { // state const user = ref(null) const token = ref('') const loading = ref(false) // getters const isLoggedIn = computed(() => !!token.value) const fullName = computed(() => user.value ? `${user.value.firstName} ${user.value.lastName}` : '' ) // actions async function login(credentials) { loading.value = true try { const res = await api.login(credentials) token.value = res.token user.value = res.user localStorage.setItem('token', res.token) } finally { loading.value = false } } function logout() { token.value = '' user.value = null localStorage.removeItem('token') } return { user, token, loading, isLoggedIn, fullName, login, logout } })
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
跨组件使用 Store
📀 vue-app
📁 src/stores/
useCartStore.js
cart.js
// stores/cart.js import { defineStore } from 'pinia' import { ref, computed } from 'vue' export const useCartStore = defineStore('cart', () => { const items = ref([]) const totalItems = computed(() => items.value.reduce((sum, item) => sum + item.quantity, 0) ) const totalPrice = computed(() => items.value.reduce((sum, item) => sum + item.price * item.quantity, 0) ) function addItem(product) { const existing = items.value.find(i => i.id === product.id) if (existing) { existing.quantity++ } else { items.value.push({ ...product, quantity: 1 }) } } function removeItem(productId) { items.value = items.value.filter(i => i.id !== productId) } function clearCart() { items.value = [] } return { items, totalItems, totalPrice, addItem, removeItem, clearCart } }) // ProductCard.vue — 添加商品到购物车 <script setup> import { useCartStore } from '@/stores/cart' const cartStore = useCartStore() function handleAddToCart(product) { cartStore.addItem(product) } </script> // Header.vue — 显示购物车数量 <script setup> import { useCartStore } from '@/stores/cart' const cartStore = useCartStore() // cartStore.totalItems 自动响应式更新 </script> <template> <div class="header-cart"> 🛒 {{ cartStore.totalItems }} </div> </template> // CartPage.vue — 显示购物车详情 <script setup> import { useCartStore } from '@/stores/cart' const cartStore = useCartStore() </script> <template> <div> <div v-for="item in cartStore.items" :key="item.id"> {{ item.name }} × {{ item.quantity }} = ¥{{ item.price * item.quantity }} <button @click="cartStore.removeItem(item.id)">删除</button> </div> <div>总计:¥{{ cartStore.totalPrice }}</div> </div> </template>
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
Pinia vs Vuex 对比
特性PiniaVuex
mutations不需要必须(同步操作)
actions直接写函数必须定义在 actions 里
嵌套模块不需要,每个 store 独立需要 modules + namespaced
TypeScript天然支持需要大量类型声明
DevTools支持支持
SSR支持支持
代码量少 30-50%模板代码多
Vue 2 支持v2 版本支持原生支持
学习曲线
Composition API完美适配适配一般
决策表
场景选什么原因
Vue 3 新项目Pinia官方推荐,完美支持 Composition API
Vue 2 新项目Vuex 4Vuex 对 Vue 2 支持更成熟
Vue 2 → Vue 3 迁移PiniaPinia 同时支持 Vue 2 和 3,可以先迁移到 Pinia 再升级 Vue
需要 TSPinia类型推导自动,不需要手动声明
大型历史 Vuex 项目保持 Vuex迁移成本高,渐进式迁移
只有父子通信都不需要props/emits 够用
只有两三个共享状态provide/inject不需要引入整个状态管理库

7. 组件通信 — 6种方式选型

props/emits = 父母和孩子对话
provide/inject = 爷爷直接给孙子零花钱
Pinia = 学校广播(全校都能听到)
EventBus = 邮局寄信(谁都能寄谁都能收)
类比理解:props/emits 就像父母和孩子对话——直接、清晰、有来有往。provide/inject 就像爷爷直接给孙子零花钱——跳过中间代,不用一层层转交。Pinia 就像学校广播——全校都能听到,适合跨区域通知。EventBus 就像邮局寄信——谁都能寄谁都能收,但信可能寄丢、可能寄重复。
方式 1:props / emits — 父子通信
📀 vue-app
📁 src/components/
Parent-props-emit.vue
Child.vue
// Parent.vue <template> <Child :message="parentMsg" :count="count" @update:count="count = $event" @submit="handleSubmit" /> </template> <script setup> import { ref } from 'vue' import Child from './Child.vue' const parentMsg = ref('来自父组件的消息') const count = ref(0) function handleSubmit(payload) { console.log('子组件提交:', payload) } </script> // Child.vue <template> <div> <p>{{ message }}</p> <p>计数:{{ count }}</p> <button @click="increment">+1</button> <button @click="submitForm">提交</button> </div> </template> <script setup> const props = defineProps({ message: { type: String, required: true }, count: { type: Number, default: 0 } }) const emit = defineEmits(['update:count', 'submit']) function increment() { emit('update:count', props.count + 1) } function submitForm() { emit('submit', { count: props.count, timestamp: Date.now() }) } </script>
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
方式 2:provide / inject — 祖孙(跨层级)
📀 vue-app
📁 src/components/
Grandparent-provide.vue
Child.vue
// Grandparent.vue(爷爷组件) <script setup> import { provide, ref, readonly } from 'vue' const theme = ref('dark') const user = ref({ name: 'Alice', role: 'admin' }) // ✅ 用 readonly 防止子组件直接修改(单向数据流) provide('theme', readonly(theme)) provide('user', readonly(user)) // 提供修改方法(而不是让子组件直接修改) provide('setTheme', (newTheme) => { theme.value = newTheme }) </script> // Grandchild.vue(孙子组件,无论多深都能 inject) <script setup> import { inject } from 'vue' // 第二个参数是默认值(找不到 provider 时使用) const theme = inject('theme', 'light') const user = inject('user', { name: '', role: 'guest' }) const setTheme = inject('setTheme', () => {}) </script> <template> <div :class="theme"> <p>当前主题:{{ theme }}</p> <p>用户:{{ user.name }}({{ user.role }})</p> <button @click="setTheme('light')">切换亮色</button> </div> </template> // 用 Symbol 做 key(推荐,避免命名冲突) // keys.js export const ThemeKey = Symbol('theme') export const UserKey = Symbol('user') // Provider provide(ThemeKey, readonly(theme)) // Injector const theme = inject(ThemeKey, 'light')
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
方式 3:Pinia — 跨组件 / 全局状态
📀 vue-app
📁 src/stores/
notification.js
cart.js
// 已经在上一节详细介绍了 Pinia // 这里展示典型的跨组件通信场景 // stores/notification.js import { defineStore } from 'pinia' import { ref, computed } from 'vue' export const useNotificationStore = defineStore('notification', () => { const notifications = ref([]) const unreadCount = computed(() => notifications.value.filter(n => !n.read).length ) function addNotification(notification) { notifications.value.push({ id: Date.now(), read: false, timestamp: new Date(), ...notification }) } function markAsRead(id) { const n = notifications.value.find(n => n.id === id) if (n) n.read = true } return { notifications, unreadCount, addNotification, markAsRead } }) // 任何组件都可以发通知 // OrderForm.vue const notificationStore = useNotificationStore() notificationStore.addNotification({ title: '新订单', message: '订单 #123 已创建' }) // 任何组件都可以读通知 // Header.vue const notificationStore = useNotificationStore() // {{ notificationStore.unreadCount }} 自动更新
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
方式 4:EventBus (mitt) — 兄弟 / 无关系组件
📀 vue-app
📁 src/utils/
eventBus.js
Comp-32.vue
// eventBus.js import mitt from 'mitt' const emitter = mitt() // 类型安全(TypeScript) type Events = { add-to-cart: { id: number; name: string; price: number } user-logged-in: { userId: number } notification: { type: string; message: string } } const emitter = mitt<Events>() export default emitter // ProductCard.vue(发送事件) <script setup> import emitter from '@/utils/eventBus' function handleAdd(product) { emitter.emit('add-to-cart', { id: product.id, name: product.name, price: product.price }) } </script> // CartSidebar.vue(接收事件) <script setup> import { onMounted, onUnmounted } from 'vue' import emitter from '@/utils/eventBus' function handleAddToCart(product) { console.log('收到加入购物车事件:', product) } onMounted(() => { emitter.on('add-to-cart', handleAddToCart) }) // ✅ 必须在卸载时移除监听,否则内存泄漏 onUnmounted(() => { emitter.off('add-to-cart', handleAddToCart) }) </script>
面试金句 Vue 3 生命周期 Hooks:onMounted(DOM 挂载完成)、onUpdated(响应式数据变更导致 DOM 更新后)、onUnmounted(组件卸载,清理定时器/事件监听)。vs Options API:beforeCreate/created 被setup()替代。注册时机:必须在 setup() 同步注册,不能在异步回调中调用。
方式 5:$refs — 父调子方法
📀 vue-app
📁 src/components/
Parent-refs.vue
Child.vue
// Parent.vue <template> <div> <button @click="validateForm">验证表单</button> <button @click="resetForm">重置表单</button> <!-- 通过 ref 获取子组件实例 --> <ChildForm ref="formRef" /> </div> </template> <script setup> import { ref } from 'vue' import ChildForm from './ChildForm.vue' const formRef = ref(null) function validateForm() { // ✅ 直接调用子组件暴露的方法 const isValid = formRef.value?.validate() if (isValid) { submitData() } } function resetForm() { formRef.value?.reset() } </script> // ChildForm.vue <script setup> import { reactive } from 'vue' const form = reactive({ name: '', email: '' }) function validate() { if (!form.name || !form.email) { showError('请填写所有字段') return false } return true } function reset() { form.name = '' form.email = '' } // ✅ 必须用 defineExpose 暴露方法,否则父组件访问不到 defineExpose({ validate, reset }) </script>
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
方式 6:$attrs — 透传属性
📀 vue-app
📁 src/components/
Parent-attrs.vue
Child.vue
// 父组件传入很多属性,中间组件不需要处理,直接透传给底层组件 // Parent.vue <template> <FormWrapper placeholder="请输入" maxlength="20" :disabled="isDisabled" class="custom-input" @input="handleInput" /> </template> // FormWrapper.vue(中间层,不需要处理每个 prop) <template> <div class="form-wrapper"> <!-- v-bind="$attrs" 把所有未声明的 props 和事件透传给子组件 --> <BaseInput v-bind="$attrs" /> </div> </template> // 如果不希望自动透传到根元素,设置 inheritAttrs: false defineOptions({ inheritAttrs: false }) // BaseInput.vue(最终接收所有属性的组件) <template> <input v-bind="$attrs" /> </template>
面试金句 defineProps 声明组件接收的 props,是编译时宏(无需 import)。TypeScript 支持:defineProps<{ title: string; count?: number }>()。Vue 3.5+ 的 响应式 props 解构允许直接解构 props 且保持响应式(const { title } = defineProps())。props 是只读的,不能在子组件中直接修改。
决策表:关系 → 方式
组件关系推荐方式备选原因
父子props/emits$refs最直观,Vue 推荐方式
祖孙(跨层级)provide/injectPinia避免 props 逐层传递
兄弟组件PiniaEventBus共享状态用 Pinia,一次性事件用 EventBus
跨组件/全局Piniaprovide/inject全局状态管理首选
无关系组件EventBusPinia一次性事件通知用 EventBus,共享状态用 Pinia
父调子方法$refs + defineExposeemits命令式调用,如表单 validate/reset
属性透传$attrs逐个传 props减少中间组件的模板代码
警告:provide/inject 的响应式问题
provide/inject 本身不是响应式的。如果 provide 一个普通值,inject 得到的也是普通值。必须 provide 一个 ref 或 reactive 对象才能保持响应式。推荐用 readonly() 包装后 provide,防止子组件直接修改。
警告:EventBus 的内存泄漏
EventBus 监听器不会自动清理。如果在 onMounted 中注册了监听器,必须在 onUnmounted 中移除,否则组件销毁后监听器仍然存在,导致内存泄漏和重复执行。推荐用 Pinia 替代大部分 EventBus 场景。

8. 路由守卫 — 用哪个守卫

beforeEach = 小区大门保安 所有进出都查(全局验证)
beforeEnter = 单元门禁 只查进这栋楼的(路由级权限)
beforeRouteLeave = 出门检查 你确定要出门吗?(离开确认)
类比理解:beforeEach 就像小区大门的保安——所有人进出都要检查(全局登录验证、权限检查)。beforeEnter 就像单元门禁——只检查进入这栋楼的人(某条路由的专属权限)。beforeRouteLeave 就像出门检查——"你确定要出门吗?东西都带了吗?"(表单未保存确认、资源清理)。
1. beforeEach — 全局前置守卫

适用:登录验证、权限控制、全局逻辑

每次路由切换都会执行,适合做全局性的检查。如:未登录用户不能访问需要认证的页面。

📀 vue-app
📁 src/router/
index.js
App.vue
// router/index.js import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ ... }) // ✅ 全局登录验证 router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') const isLoggedIn = !!token // 白名单路由:不需要登录就能访问 const whiteList = ['/login', '/register', '/forgot-password'] if (whiteList.includes(to.path)) { // 已登录用户访问登录页,重定向到首页 if (isLoggedIn) { next({ path: '/' }) } else { next() } } else { // 需要认证的页面 if (isLoggedIn) { next() } else { // ✅ 保存原目标路由,登录后跳回去 next({ path: '/login', query: { redirect: to.fullPath } }) } } }) // ✅ 全局权限检查 router.beforeEach(async (to, from, next) => { const userStore = useUserStore() // 需要权限的路由 if (to.meta.requiresAuth) { if (!userStore.isLoggedIn) { next('/login') return } // 需要特定角色 if (to.meta.requiredRole) { if (userStore.role !== to.meta.requiredRole) { next('/403') return } } } next() })
面试金句 beforeEach 是全局前置守卫,每次路由切换前执行,适合做登录验证和权限控制。返回 false 或调用 next(false) 会取消导航。Vue Router 4 推荐用 return 代替 next() 参数。
2. beforeEnter — 路由级前置守卫

适用:特定路由的权限控制、数据预加载

只在进入特定路由时执行,适合该路由独有的逻辑。如:管理员页面只允许 admin 角色进入。

📀 vue-app
📁 src/router/
routes.js
index.js
const routes = [ { path: '/admin', component: () => import('../views/Admin.vue'), meta: { requiresAuth: true }, beforeEnter: (to, from, next) => { const userStore = useUserStore() // ✅ 只有管理员能进入这个路由 if (userStore.role === 'admin') { next() } else { next('/403') } } }, { path: '/editor/:id', component: () => import('../views/Editor.vue'), beforeEnter: async (to, from, next) => { // ✅ 进入编辑器前预加载数据 try { const doc = await api.getDocument(to.params.id) // 把预加载的数据存到路由 meta 中 to.meta.document = doc next() } catch (error) { next({ path: '/404' }) } } } ]
面试金句 defineAsyncComponent 定义异步组件,实现组件级代码分割。Vue 3 写法:defineAsyncComponent(() => import("Comp.vue"))。高级选项:loadingComponent、errorComponent、delay、timeout。搭配 Suspense 统一管理异步组件加载状态。
3. beforeRouteEnter — 组件内守卫(进入时)

适用:组件级权限检查、数据预加载

定义在组件内部,此时组件实例还未创建,拿不到 this。需要通过 next(vm => {}) 回调访问组件实例。

📀 vue-app
📁 src/views/
UserDetail.vue
UserList.vue
// UserDetail.vue <script setup> import { ref, onMounted } from 'vue' import { onBeforeRouteEnter, onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router' const userData = ref(null) // ✅ 进入组件前(组件实例还没创建) // ⚠️ 拿不到 this,因为组件还没创建 onBeforeRouteEnter(async (to, from) => { // 可以在这里做权限检查 if (!hasPermission('user:view')) { return '/403' } // 可以预加载数据 const data = await api.getUser(to.params.id) to.meta.userData = data }) // ✅ 路由参数更新时(/user/1 → /user/2) onBeforeRouteUpdate(async (to, from) => { // 组件实例已存在,可以直接访问 userData.value = await api.getUser(to.params.id) }) // ✅ 离开组件前 onBeforeRouteLeave((to, from) => { // 见下面 beforeRouteLeave 详解 }) </script> // Options API 写法(拿到组件实例) export default { beforeRouteEnter(to, from, next) { // ⚠️ 拿不到 this // ✅ 通过 next 的回调拿到组件实例 next(vm => { vm.loadData(to.params.id) }) } }
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
4. beforeRouteUpdate — 同一路由不同参数

适用:/user/1 → /user/2 同一组件不同参数

复用组件时,路由参数变化不会重新创建组件。需要在 beforeRouteUpdate 中手动刷新数据。

📀 vue-app
📁 src/views/
UserDetail.vue
UserList.vue
// UserDetail.vue <script setup> import { ref } from 'vue' import { onBeforeRouteUpdate } from 'vue-router' const userData = ref(null) const loading = ref(false) async function loadUser(id) { loading.value = true try { userData.value = await api.getUser(id) } finally { loading.value = false } } // ✅ /user/1 → /user/2 时触发 // 组件不会重新创建,需要手动刷新数据 onBeforeRouteUpdate(async (to, from) => { if (to.params.id !== from.params.id) { await loadUser(to.params.id) } }) </script>
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
5. beforeRouteLeave — 离开确认

适用:表单未保存确认、离开前清理资源

最常见的用法:用户编辑了表单但未保存就要离开,弹出确认对话框。

📀 vue-app
📁 src/views/
EditForm.vue
UserList.vue
// EditForm.vue <script setup> import { ref } from 'vue' import { onBeforeRouteLeave } from 'vue-router' const isDirty = ref(false) // 表单是否有未保存的修改 let pollingTimer = null // ✅ 表单未保存确认 onBeforeRouteLeave((to, from) => { if (isDirty.value) { const answer = confirm( '您有未保存的修改,确定要离开吗?离开后修改将丢失。' ) if (!answer) { return false // 取消导航 } } // ✅ 离开前清理资源 if (pollingTimer) { clearInterval(pollingTimer) pollingTimer = null } // 允许离开 return true }) </script> // 更优雅的确认方式(使用自定义对话框而非 confirm) onBeforeRouteLeave((to, from) => { if (isDirty.value) { // 返回 Promise,等待用户操作 return new Promise((resolve) => { showConfirmDialog({ title: '未保存的修改', message: '您有未保存的修改,确定要离开吗?', onConfirm: () => resolve(true), onCancel: () => resolve(false) }) }) } })
面试金句 ref 用于创建基本类型的响应式数据(String/Number/Boolean),必须通过 .value 访问(模板中自动解包)。ref vs reactive 的选择:简单值用 ref,对象/数组用 reactive。但 ref 也可存对象——内部会自动调用 reactive 包装。实际开发中推荐统一用 ref,避免 reactive 的解构丢失响应式问题。
完整示例:登录拦截 + 权限控制
📀 vue-app
📁 src/router/
guards.js
index.js
// router/guards.js import { useUserStore } from '@/stores/user' export function setupRouterGuards(router) { // 1. 全局前置守卫:登录验证 + 权限检查 router.beforeEach(async (to, from) => { const userStore = useUserStore() // 白名单 const whiteList = ['/login', '/register', '/404', '/403'] if (whiteList.includes(to.path)) { return true } // 未登录 → 跳登录页 if (!userStore.isLoggedIn) { return { path: '/login', query: { redirect: to.fullPath } } } // 首次进入,获取用户信息 if (!userStore.userInfo) { try { await userStore.fetchUserInfo() } catch { userStore.logout() return { path: '/login' } } } // 权限检查 if (to.meta.requiredRole) { if (userStore.role !== to.meta.requiredRole) { return { path: '/403' } } } return true }) // 2. 全局后置钩子:设置页面标题、上报等 router.afterEach((to, from) => { // 设置页面标题 document.title = to.meta.title ? `${to.meta.title} - MyApp` : 'MyApp' // 页面访问上报 reportPageView(to.fullPath) }) } // router/index.js import { createRouter, createWebHistory } from 'vue-router' import { setupRouterGuards } from './guards' const router = createRouter({ history: createWebHistory(), routes: [ { path: '/login', component: () => import('@/views/Login.vue'), meta: { title: '登录' } }, { path: '/', component: () => import('@/views/Home.vue'), meta: { title: '首页', requiresAuth: true } }, { path: '/admin', component: () => import('@/views/Admin.vue'), meta: { title: '管理后台', requiresAuth: true, requiredRole: 'admin' } }, { path: '/profile', component: () => import('@/views/Profile.vue'), meta: { title: '个人资料', requiresAuth: true } } ] }) setupRouterGuards(router) export default router
面试金句 defineAsyncComponent 定义异步组件,实现组件级代码分割。Vue 3 写法:defineAsyncComponent(() => import("Comp.vue"))。高级选项:loadingComponent、errorComponent、delay、timeout。搭配 Suspense 统一管理异步组件加载状态。
决策表:需求 → 守卫
需求用哪个守卫原因
全局登录验证beforeEach所有路由都需要检查
全局权限控制beforeEach统一处理角色权限
特定路由权限beforeEnter只对某个路由做检查
路由级数据预加载beforeEnter进入该路由前获取数据
组件级权限检查beforeRouteEnter在组件内部定义逻辑
同路由不同参数beforeRouteUpdate组件复用,参数变化时刷新
表单未保存确认beforeRouteLeave离开前拦截,防止数据丢失
离开前清理资源beforeRouteLeave清理定时器、WebSocket等
设置页面标题afterEach导航完成后更新标题
警告:next() 必须调用
在 Vue Router 4 中,推荐用 return 代替 next()。但如果使用 next(),必须确保每条逻辑路径都调用了 next(),否则路由导航会被卡住(页面不动,也不报错)。特别注意 if/else 分支和 try/catch 中的所有路径。
警告:异步守卫
异步守卫(如 await API 调用)期间,用户可能快速点击其他链接,导致多个导航同时进行。建议在 beforeEach 中检查导航是否被中断。Vue Router 4 提供了 NavigationFailure 来处理这种情况。
警告:无限重定向
守卫中重定向时要避免循环。如:beforeEach 中未登录就跳 /login,但 /login 的守卫又检查了什么导致再次重定向。确保白名单路由不会触发认证检查。
📀 vue-app
📁 src/router/
pitfall-redirect.js
guards.js
// ❌ 无限重定向的典型错误 router.beforeEach((to, from, next) => { const isLoggedIn = false if (!isLoggedIn) { next('/login') // → 触发 /login 的 beforeEach // → 又不满足条件 → 又 next('/login') // → 无限循环! } else { next() } }) // ✅ 正确:白名单排除 router.beforeEach((to, from, next) => { const isLoggedIn = false const whiteList = ['/login', '/register'] if (whiteList.includes(to.path) || isLoggedIn) { next() } else { next('/login') // /login 在白名单里,不会再次触发重定向 } }) // ✅ Vue Router 4 推荐写法(用 return 代替 next) router.beforeEach((to, from) => { const isLoggedIn = false const whiteList = ['/login', '/register'] if (whiteList.includes(to.path) || isLoggedIn) { return true // 允许导航 } return { // 重定向 path: '/login', query: { redirect: to.fullPath } } })
面试金句 场景:决策表:需求 → 守卫
需求用哪个守卫原因
全局登录验证beforeEach所有路由都需要检查
全局权限控制beforeEach统一处理角色权限
特定路由权限beforeEnter只对某个路由做检查
路由级数据预加载beforeEnter进入该路由前获取数据
组件级权限检查beforeRouteEnter在组件内部定义逻辑
同路由不同参数beforeRouteUpdate组件复用,参数变化时刷新
表单未保存确认beforeRouteLeave离开前拦截,防止数据丢失
离开前清理资源beforeRouteLeave清理定时器、WebSocket等
设置页面标题afterEach导航完成后更新标题
警告:next() 必须调用
在 Vue Router 4 中,推荐用 return 代替 next()。但如果使用 next(),必须确保每条逻辑路径都调用了 next(),否则路由导航会被卡住(页面不动,也不报错)。特别注意 if/else 分支和 try/catch 中的所有路径。
警告:异步守卫
异步守卫(如 await API 调用)期间,用户可能快速点击其他链接,导致多个导航同时进行。建议在 beforeEach 中检查导航是否被中断。Vue Router 4 提供了 NavigationFailure 来处理这种情况。
警告:无限重定向
守卫中重定向时要避免循环。如:beforeEach 中未登录就跳 /login,但 /login 的守卫又检查了什么导致再次重定向。确保白名单路由不会触发认证检查。

Vue 3 场景决策 — 什么时候用 / 什么时候不用 / 边界

← 返回场景决策