平时用 Vue 生态,我一般先看数据放哪、路由怎么切。Composition API、Pinia、Router 的设计动机,基本都是围绕这两件事展开的。
Q1: Composition API 解决了什么问题?
Options API 的逻辑碎片化
data 放数据、methods 放方法、computed 放计算属性、watch 放侦听器。当组件变大时,同一个功能的代码被拆得七零八落—比如”搜索功能’的输入框状态在 data、搜索方法在 methods、防抖逻辑在 watch、结果缓存在 computed。Mixins 的致命缺陷
Composition API 的逻辑聚合
setup() 函数和 Composables(组合函数),逻辑复用从”Mixin 的隐式合并”升级为”Composable 的显式调用”—类型安全、来源清晰、无命名冲突。Composables 的工程实践
useSearch)可以将状态定义(ref)、计算属性(computed)、方法和侦听器(watch)全部封装在一个函数中,返回值是一个普通对象—外部通过解构获取 query、results、search 等接口。多个 Composable 可以相互组合(如 useSearch 内部调用 useDebounce),形成清晰的逻辑层次。关键启示:按功能组织比按类型组织更可维护
Options API + Mixins
Composition API + Composables
Q2: Vuex 和 Pinia 有什么区别?
Pinia 是 Vue 3 官方推荐的状态管理库,是 Vuex 的继任者。Q3: Composition API 和 Options API 到底该选哪个?
很多人知道 Composition API “更灵活”,但说不清在什么场景下 Options API 反而更好。基础梳理
专家视角深度剖析
很多人觉得 Composition API 一定比 Options API 好,但Vue 官方明确表示:Options API 不会被废弃。 Options API 的优势在于低认知负担。对于简单组件(一个列表、一个表单),Options API 的 data/methods/computed 分类方式更直观——你不需要思考”这个功能的代码应该怎么组织”,因为 Vue 已经帮你分好了。Composition API 的ref、reactive、computed、watch 需要你理解响应式原理,否则很容易写出 ref({ count: 0 }) 这种不必要的嵌套。
但当组件复杂度超过一个临界点(大约 3-5 个功能点),Options API 的劣势就暴露了——同一个功能的代码被拆得七零八落,维护成本指数级增长。此时 Composition API 的”按功能聚合”优势就显现出来。
Q4: Vuex 的 mutations 到底有什么用?Pinia 为什么能移除它?
Vuex 的设计初衷: 可追踪的状态变更
实践中的问题: 纯粹的样板代码
commit('SET_USER', user)、commit('INCREMENT')。开发者总是先在 actions 中处理异步逻辑,然后 commit 一个 mutation 来修改 state。这个中间层增加了代码量,但没有提供额外的价值。Pinia 的方案: Proxy 拦截替代 mutations
reactive(Proxy)包装 state。Proxy 可以拦截所有属性的修改,DevTools 通过 Proxy 拦截就能追踪每次状态变更——不需要 mutations 这个中间层。代码更简洁,调试能力不丢。关键洞察: mutations 的问题不是设计缺陷, 而是时代局限
defineProperty 只能拦截已有属性,无法追踪新增属性。mutations 是一种”显式声明”的追踪方式——你必须告诉我你要改什么,我才能记录。Vue 3 的 Proxy 让”自动追踪”成为可能,mutations 就变得多余了。Vuex 的 mutations
commit('MUTATION_NAME', payload)。就像公司规定每次报销都必须先填纸质申请单(mutation),再交给财务(commit),即使金额只有 10 块钱。Pinia 的直接修改
this.user = user。就像公司升级了电子审批系统——你直接在系统里提交报销(直接修改 state),系统自动记录谁在什么时候改了什么(Proxy 拦截),不需要纸质申请单。Q5: Vue Router 的导航守卫有哪些?
Vue Router 提供了三种级别的导航守卫:专家视角深度剖析
很多人知道导航守卫有全局、路由、组件三个级别,但说不清它们的执行顺序和典型用途。 导航守卫的核心用途是权限控制和数据预加载。beforeEach 适合做全局的登录检查—每次路由切换前检查 token,未登录就跳转登录页。beforeEnter 适合做路由级别的权限—某些路由需要特定角色才能访问。onBeforeRouteLeave 适合做页面离开确认—用户填写了表单但未保存,离开前提醒。
典型的权限校验场景:
Q6: Vue Router 的路由模式有哪些?
Q7: Vue Router 的路由匹配是怎么实现的?
路由表的构建
routes 数组)编译成一个路由记录树。每个路由记录包含路径(path)、组件(component)、子路由(children)、守卫(beforeEnter)等信息。路径匹配: 从字符串到正则
/user/:id)编译成正则表达式。当 URL 变化时,用正则逐一匹配路由记录,找到匹配的路由。支持参数(:id)、通配符(*)、可选参数(:id?)等语法。导航解析流程
history 模式的实现: pushState + popstate
history.pushState() 改变 URL(不触发页面刷新),用 popstate 事件监听浏览器前进/后退。Vue Router 还拦截了 <router-link> 的点击事件,调用 pushState 而不是让浏览器默认跳转。关键洞察: 匹配按路径具体程度排序
createRouterMatcher)时,会为每条路由计算 score 并排序:静态段优先于参数段,参数段优先于通配符。匹配时按分数从高到低尝试,因此 /user/me 一般不会被 /user/:id 抢先匹配。不过同分场景仍受定义顺序影响,复杂路由建议把更具体的写在前面。传统多页面应用
Vue Router 单页面路由
底层源码:Pinia 的 store 是怎么实现的?
reactive 包装 store 的 state,用 computed 实现 getters,用普通函数实现 actions(详见 Pinia 源码 packages/pinia/src/store.ts):
reactive,getters 是 computed,actions 是普通函数。Vuex 需要 mutations 来追踪变化,Pinia 直接用 Proxy 拦截 state 的修改,DevTools 一样可以追踪。
history.pushState API(详见 Vue Router 源码 src/history/html5.ts)。pushState 可以改变浏览器地址栏的 URL,但不会触发页面刷新。Vue Router 监听 popstate 事件(用户点击浏览器前进/后退按钮时触发),在事件回调中匹配路由并渲染对应的组件。
十、及格回答 vs 满分回答
问题:“Composition API 解决了什么问题?”
及格回答(60-70分):“Composition API 让逻辑复用更方便,可以抽离 composable 函数。比 Mixins 好,不会命名冲突。”问题出在:知道好处但不理解痛点。 满分回答(90-100分):
Composition API 解决的核心问题是Options API 的逻辑碎片化。在 Options API 中,一个功能的代码被分散在 data、methods、computed、watch 等不同选项中。比如一个”搜索功能”—输入框状态在data、搜索方法在methods、防抖逻辑在watch、结果缓存在computed。当组件有 5 个这样的功能时,你必须在不同选项之间跳来跳去,维护极其困难。[1]
Vue 2 用 Mixins 来复用逻辑,但 Mixins 有三个致命缺陷。命名冲突:两个 Mixins 定义同名属性会互相覆盖,Vue 2 在mergeOptions(src/core/util/options.js)中按数组顺序合并,后者覆盖前者,没有任何警告。来源不明:组件用了 5 个 Mixin,某个属性来自哪个?你必须全局搜索。隐式依赖:Mixins 之间可能互相依赖(一个 Mixin 访问了另一个 Mixin 定义的this.xxx),但这种依赖关系完全隐藏在代码中。[2]
Composition API 通过 Composables(组合函数)解决了这些问题。一个 Composable 把同一个功能的所有逻辑聚合在一个函数中,通过显式返回值暴露给组件。const { query, results, search } = useSearch()—你一眼就知道query来自useSearch。TypeScript 也能完美推断类型—query的类型是Ref<string>,results的类型是Ref<SearchResult[]>。在 Vue 3 中,setup()执行时会激活组件实例对应的effectScope(packages/runtime-core/src/component.ts的setCurrentInstance),computed、watch等创建的 effect 会归入这个 scope,组件卸载时调用scope.stop()统一清理。[3]
实际项目中的建议:新项目全部用 Composition API +<script setup>。<script setup>是setup()的语法糖,编译器会自动把顶层变量暴露给模板,不需要手动return。老项目渐进式迁移—先用 Composables 替换 Mixins,再逐步把组件迁移到<script setup>。不要一次性重写所有组件。
常见追问
Vuex 的 mutations 到底有什么用?Pinia 为什么移除了?
Vuex 的 mutations 到底有什么用?Pinia 为什么移除了?
Pinia 的 getters 和 computed 有什么区别?
Pinia 的 getters 和 computed 有什么区别?
computed(packages/pinia/src/store.ts 中的 createSetupStore)。但 Pinia 做了一层封装:1)自动绑定 this 到当前 store,可以直接访问其他 getters 和 state;2)支持传参——返回一个函数而不是值;3)支持在其他 getters 中引用。computed 的区别是:Pinia 的 getters 是 store 级别的缓存,多个组件共享同一个 store 时,getter 只计算一次。”Pinia 的 storeToRefs 是什么?为什么需要它?
Pinia 的 storeToRefs 是什么?为什么需要它?
storeToRefs(packages/pinia/src/store.ts)内部遍历 store 的 state 和 getters,把每个属性转为 ref。actions 不需要转换——actions 是普通函数,不涉及响应式。”Vue Router 的 hash 和 history 模式有什么区别?
Vue Router 的 hash 和 history 模式有什么区别?
#(如 /#/about),通过监听 hashchange 事件实现路由切换,不会触发页面刷新,兼容性好。history 模式的 URL 干净(如 /about),通过 history.pushState 改变 URL + 监听 popstate 事件实现。但 history 模式刷新时浏览器会向服务器请求这个 URL,服务器必须配置返回 index.html,否则会 404。源码层面(src/history/html5.ts),history 模式的 push 方法调用 history.pushState(state, '', url) 改变 URL,然后调用 transitionTo 匹配路由。popstate 监听浏览器前进/后退按钮。hash 模式(src/history/hash.ts)则监听 hashchange 事件,从 location.hash 中提取路径。”Vue Router 的导航守卫执行顺序?
Vue Router 的导航守卫执行顺序?
onBeforeRouteEnter 的 next 回调中才能访问组件实例(next(vm => { ... })),因为此时组件还没有创建。onBeforeRouteLeave 常用于表单未保存确认。”嵌套路由是怎么渲染的?
嵌套路由是怎么渲染的?
<router-view> 中,支持多层嵌套。高分回答:
“嵌套路由通过在父组件中放置 <router-view> 来渲染子路由。Vue Router 的源码中(src/routerView.ts),<router-view> 组件会根据当前匹配的路由记录链(matched array),找到对应层级的组件来渲染。路由配置:matched 数组,包含从根路由到当前路由的所有记录。每个 <router-view> 根据自己的深度(depth)从 matched 数组中取出对应的组件来渲染。”路由懒加载是怎么实现的?
路由懒加载是怎么实现的?
import() 实现:import() 返回一个 Promise。Vue Router 在匹配到该路由时,会调用这个函数加载组件。加载完成后,Vite/webpack 自动把该组件分割为单独的 chunk 文件。Vue Router 内部对懒加载做了优化:如果同一个路由组件被多个路由使用,会缓存 Promise 的结果,避免重复加载。在源码中(src/utils/resolveComponents.ts),resolveComponent 函数负责处理异步组件的加载和缓存。”Composables 和 Mixins 的区别?
Composables 和 Mixins 的区别?
const { count, increment } = useCounter(),你一眼就知道 count 来自 useCounter。Composables 还支持 TypeScript 类型推断,Mixins 做不到。更深层的区别:Mixins 的生命周期钩子会和组件的钩子合并执行(Vue 2 在 mergeOptions 中合并),Composables 的生命周期钩子直接注册到组件实例上(在 setup() 中调用 onMounted 等)。Composables 之间可以相互组合(useSearch 内部调用 useDebounce),形成清晰的调用链。Mixins 之间如果有隐式依赖(一个 Mixin 访问了另一个 Mixin 的 this.xxx),这种关系完全隐藏在代码中,极难维护。”Composition API 有哪些常见的错误用法?
Composition API 有哪些常见的错误用法?
ref({ count: 0 }) 不如 reactive({ count: 0 }) 直接,原始值才需要 ref。
2)在条件语句中使用 ref:if (condition) { const x = ref(0) }——Vue 的响应式追踪依赖调用顺序,条件语句会导致追踪不稳定。
3)忘记 .value:在 JS 中访问 count 而不是 count.value,不会触发依赖收集。
4)在异步操作后才创建 ref:响应式数据必须在 setup() 同步执行时创建,异步创建的 ref 不会被 Vue 追踪。
5)解构 reactive 丢失响应式:const { count } = reactive({ count: 0 }) 会丢失响应式,应该用 toRefs。”Composition API 的 setup() 和 Options API 的 created 谁先执行?
Composition API 的 setup() 和 Options API 的 created 谁先执行?
setup() 在 beforeCreate 之前执行,自然也比 created 更早。执行 setup() 时组件实例已经创建,但 setup 的签名不暴露实例代理,所以拿不到 this。这也是为什么 Composition API 用 ref()、reactive() 来声明状态,而不是用 data() 返回对象。Vue 3 的 Options API 中,created 钩子依然存在,执行时机不变。”Options API 和 Composition API 能混用吗?
Options API 和 Composition API 能混用吗?
setup() 函数(或 <script setup>)来使用 Composition API,同时保留 data、methods、computed 等选项。Vue 会把 setup() 的返回值和 Options API 的选项合并。但官方不推荐混用:1)代码风格不一致,维护困难;2)setup() 中没有 this,Options API 依赖 this,两种访问方式混在一起容易混淆;3)TypeScript 类型推断在混用时效果较差。最佳实践是:新组件全部用 <script setup>,老组件逐步迁移。”Vuex 和 Pinia 的模块化有什么区别?
Vuex 和 Pinia 的模块化有什么区别?
namespaced: true 避免命名冲突。访问时用 store.state.user.name 或 mapState('user', ['name'])。嵌套层级深了之后,路径非常长,类型推断也很难做。Pinia 的模块化是扁平的——每个 store 是一个独立的实体,有自己的 id、state、getters、actions。访问时直接 const userStore = useUserStore()。store 之间可以相互引用:Q8: Vuex 的原理详解
Vuex 的核心架构
Vuex 的设计借鉴了 Flux/Redux 的单向数据流思想,但针对 Vue 的响应式系统做了优化。Vuex 的响应式实现
Vuex 的 state 本身就是 Vue 的响应式对象。当组件通过this.$store.state 访问状态时,Vue 的依赖收集机制会自动追踪。当 state 变化时,依赖该状态的组件自动更新。
mutations 的设计初衷
mutations 存在的目的是可追踪的状态变更。每次修改 state 都通过 mutation,DevTools 可以记录每次变化的类型和载荷(payload),支持时间旅行调试(time travel debugging)。Q9: Pinia 的原理详解
Pinia 的核心架构
Pinia 是 Vue 3 官方推荐的状态管理库,它的设计哲学是没有魔法,就是 Vue 原生的响应式 API。Pinia 的 store 创建流程
Pinia 为什么不需要 mutations?
Pinia 用 Vue 3 的reactive(Proxy)包装 state。Proxy 可以拦截所有属性的修改,DevTools 通过 Proxy 拦截就能追踪每次状态变更——不需要 mutations 这个中间层。
Q10: Vuex vs Pinia 技术选型指南
核心区别总结
选型决策树
什么时候 Vuex 仍然更好?
- 团队已熟悉 Vuex:迁移成本大于收益
- 需要严格的变更追踪:mutations 的显式声明让状态变更有迹可循
- 大型团队协作:mutations 作为”变更契约”,团队成员必须遵守
什么时候 Pinia 更好?
- 新项目:Pinia 是 Vue 3 的官方推荐
- TypeScript 优先:Pinia 的类型推断更完善
- 简洁优先:没有 mutations 的样板代码
- 模块化需求:扁平化 store 更易维护
Q11: 简易实现 Vuex
Q12: 简易实现 Pinia
延伸阅读与引用参考
- Vue 3 官方文档 - Composition API FAQ:Vue 官方对 Composition API 设计初衷的权威解释。[1]
- Pinia 源码 packages/pinia/src/store.ts:Pinia store 的核心实现。[2]
- Vue Router 源码 src/history/html5.ts:history 模式的实现。[3]
- Pinia 官方文档:Pinia 的官方教程,包含从 Vuex 迁移的指南。
