Skip to main content
平时用 Vue 生态,我一般先看数据放哪、路由怎么切。Composition API、Pinia、Router 的设计动机,基本都是围绕这两件事展开的。

Q1: Composition API 解决了什么问题?

1

Options API 的逻辑碎片化

Options API 把组件的逻辑按选项分类:data 放数据、methods 放方法、computed 放计算属性、watch 放侦听器。当组件变大时,同一个功能的代码被拆得七零八落—比如”搜索功能’的输入框状态在 data、搜索方法在 methods、防抖逻辑在 watch、结果缓存在 computed。
2

Mixins 的致命缺陷

Vue 2 用 Mixins 复用逻辑,但 Mixins 有三个致命问题:命名冲突(两个 Mixins 定义同名属性会互相覆盖)、来源不明(组件用了 5 个 Mixin,某个属性来自哪个?)、隐式依赖(Mixins 之间可能互相依赖)。
3

Composition API 的逻辑聚合

Composition API 让同一个功能的代码可以物理聚合在一起。通过 setup() 函数和 Composables(组合函数),逻辑复用从”Mixin 的隐式合并”升级为”Composable 的显式调用”—类型安全、来源清晰、无命名冲突。
4

Composables 的工程实践

一个典型的 Composable(如 useSearch)可以将状态定义(ref)、计算属性(computed)、方法和侦听器(watch)全部封装在一个函数中,返回值是一个普通对象—外部通过解构获取 query、results、search 等接口。多个 Composable 可以相互组合(如 useSearch 内部调用 useDebounce),形成清晰的逻辑层次。
5

关键启示:按功能组织比按类型组织更可维护

Composition API 的设计哲学是关注点分离—不是把所有 data 放一起、所有 methods 放一起,而是把”搜索功能”的所有相关代码放一起。这种思路在大型项目中尤其重要,因为它让代码的可读性和可维护性随组件复杂度线性增长,而不是指数级恶化。
Composition API 解决的核心问题:
一句话总结:Composition API 让组件逻辑从”按选项分类”变成”按功能聚合”,解决了 Options API 的碎片化和 Mixins 的命名冲突。

Options API + Mixins

同一个功能的代码分散在 data/methods/computed/watch 中。就像一本菜谱,所有菜的材料、步骤、注意事项混在一起—找”红烧肉的做法’要翻遍整本书。

Composition API + Composables

同一个功能的代码聚合在一个 Composable 函数中。就像每道菜单独一页—“红烧肉’的材料、步骤、注意事项全在一页纸上,一目了然。
回答时重点说痛点(Options API 的碎片化和 Mixins 的缺陷)和解决方案(Composables 的逻辑聚合),不要只说”Composition API 更灵活’—想听的是你对为什么要这样设计的理解。

Q2: Vuex 和 Pinia 有什么区别?

Pinia 是 Vue 3 官方推荐的状态管理库,是 Vuex 的继任者。
Vuex 的 mutations 存在的初衷是追踪状态变化—每次修改 state 都通过 mutation,DevTools 可以记录每次变化。但实践中,mutations 只是样板代码—你总是写 commit('SET_USER', user) 这种固定模式,没有额外价值。Pinia 直接在 actions 中修改 state,DevTools 依然可以追踪变化(通过 Proxy 拦截)。这让代码更简洁,同时不丢失调试能力。
Vuex 和 Pinia 的核心区别是”是否需要 mutations 这个中间层”—Vuex 必须通过 mutations 修改 state(可追踪但繁琐),Pinia 直接在 actions 中修改(简洁且可追踪)。 就像审批流程。Vuex 就像严格的审批—你要改一个文件(修改 state),必须先填申请单(mutation),再交给领导审批(commit)。Pinia 就像扁平化管理—你直接改文件(在 action 中修改 state),系统自动记录谁改了什么(DevTools 追踪)。流程更简洁,但可追溯性不丢。

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 的”按功能聚合”优势就显现出来。
Options API 就像按衣服类型整理——上衣放一格、裤子放一格、袜子放一格。衣柜小的时候很整齐,但当你有 50 件衣服时,找”明天穿的那套”要翻遍好几个格子。Composition API 就像按场合整理——“上班穿的”放一起、“运动穿的”放一起、“约会穿的”放一起。找”明天上班穿的”直接去一个格子就行。但如果只有 5 件衣服,按类型整理反而更简单。

Q4: Vuex 的 mutations 到底有什么用?Pinia 为什么能移除它?

1

Vuex 的设计初衷: 可追踪的状态变更

Vuex 引入 mutations 的目的是让每一次状态变更都有迹可循。每次修改 state 都必须通过 mutation,DevTools 可以记录每次变化的类型和载荷(payload),方便调试和时间旅行(time travel)。
2

实践中的问题: 纯粹的样板代码

在实际项目中,mutations 只是固定的模式——commit('SET_USER', user)、commit('INCREMENT')。开发者总是先在 actions 中处理异步逻辑,然后 commit 一个 mutation 来修改 state。这个中间层增加了代码量,但没有提供额外的价值。
3

Pinia 的方案: Proxy 拦截替代 mutations

Pinia 用 Vue 3 的 reactive(Proxy)包装 state。Proxy 可以拦截所有属性的修改,DevTools 通过 Proxy 拦截就能追踪每次状态变更——不需要 mutations 这个中间层。代码更简洁,调试能力不丢。
4

关键洞察: mutations 的问题不是设计缺陷, 而是时代局限

Vuex 诞生于 Vue 2 时代,那时候 defineProperty 只能拦截已有属性,无法追踪新增属性。mutations 是一种”显式声明”的追踪方式——你必须告诉我你要改什么,我才能记录。Vue 3 的 Proxy 让”自动追踪”成为可能,mutations 就变得多余了。
mutations 的演进:
一句话总结:Vuex 的 mutations 是 Vue 2 时代”显式追踪”的妥协方案,Pinia 借助 Proxy 实现了”自动追踪”,mutations 自然被淘汰。

Vuex 的 mutations

每次修改 state 都要写 commit('MUTATION_NAME', payload)。就像公司规定每次报销都必须先填纸质申请单(mutation),再交给财务(commit),即使金额只有 10 块钱。

Pinia 的直接修改

在 action 中直接 this.user = user。就像公司升级了电子审批系统——你直接在系统里提交报销(直接修改 state),系统自动记录谁在什么时候改了什么(Proxy 拦截),不需要纸质申请单。
回答时先说 mutations 的设计初衷(追踪状态变更),再说实践中的问题(样板代码),最后说 Pinia 的替代方案(Proxy 自动追踪)。想听的是你对”为什么当初这样设计”和”为什么现在不需要了”的理解,而不是简单地说”Pinia 更简洁”。

Q5: Vue Router 的导航守卫有哪些?

Vue Router 提供了三种级别的导航守卫:

专家视角深度剖析

很多人知道导航守卫有全局、路由、组件三个级别,但说不清它们的执行顺序和典型用途。 导航守卫的核心用途是权限控制和数据预加载。beforeEach 适合做全局的登录检查—每次路由切换前检查 token,未登录就跳转登录页。beforeEnter 适合做路由级别的权限—某些路由需要特定角色才能访问。onBeforeRouteLeave 适合做页面离开确认—用户填写了表单但未保存,离开前提醒。 典型的权限校验场景:
导航守卫的执行顺序:

Q6: Vue Router 的路由模式有哪些?

history 模式的 URL 没有 #,看起来更干净,但有一个致命问题:刷新页面时,浏览器会向服务器请求这个 URL。比如用户访问 /about,服务器没有这个路由,会返回 404。解决方案:
  1. 服务器配置:所有路由都返回 index.html(Nginx 的 try_files)
  2. 前端兜底:在 404 页面重定向到首页
路由模式的核心区别是”刷新时浏览器是否向服务器请求”—hash 模式用 # 分隔,浏览器忽略 # 后面的部分;history 模式用真实 URL,刷新时会请求服务器。 就像两种门牌号。hash 模式就像公寓的门牌号—/#/302,浏览器只认 # 前面的地址(大楼地址),# 后面的由前端处理。刷新时浏览器不会向服务器请求 /#/302。history 模式就像独立别墅的地址—/302,看起来更干净,但刷新时浏览器会向服务器请求 /302,服务器必须配置好返回 index.html。

Q7: Vue Router 的路由匹配是怎么实现的?

1

路由表的构建

Vue Router 在初始化时会把路由配置(routes 数组)编译成一个路由记录树。每个路由记录包含路径(path)、组件(component)、子路由(children)、守卫(beforeEnter)等信息。
2

路径匹配: 从字符串到正则

Vue Router 把路由路径(如 /user/:id)编译成正则表达式。当 URL 变化时,用正则逐一匹配路由记录,找到匹配的路由。支持参数(:id)、通配符(*)、可选参数(:id?)等语法。
3

导航解析流程

URL 变化 → 触发导航守卫链 → 匹配路由 → 解析异步组件 → 更新视图。整个流程是异步的,支持在守卫中做异步操作(如检查登录状态)。
4

history 模式的实现: pushState + popstate

history 模式用 history.pushState() 改变 URL(不触发页面刷新),用 popstate 事件监听浏览器前进/后退。Vue Router 还拦截了 <router-link> 的点击事件,调用 pushState 而不是让浏览器默认跳转。
5

关键洞察: 匹配按路径具体程度排序

Vue Router 4 在构建匹配器(createRouterMatcher)时,会为每条路由计算 score 并排序:静态段优先于参数段,参数段优先于通配符。匹配时按分数从高到低尝试,因此 /user/me 一般不会被 /user/:id 抢先匹配。不过同分场景仍受定义顺序影响,复杂路由建议把更具体的写在前面。
Vue Router 的核心流程:
一句话总结:Vue Router 的核心是”把路径编译成正则 + 导航守卫链”,通过拦截 URL 变化来驱动组件切换。

传统多页面应用

每次跳转都是服务器返回新页面,整个页面重新加载。就像你每次换房间都要走出大楼再走进来——所有状态都丢失了。

Vue Router 单页面路由

URL 变化时只更新页面中的组件,不触发整页刷新。就像你在一个大楼里换房间——大楼不变(页面不刷新),只是里面的房间布局变了(组件切换)。
回答时重点说”路径编译成正则”和”导航守卫链”这两个核心机制。可能会被追问”history 模式刷新 404 怎么解决”——服务器配置 try_files $uri /index.html。不要上来就背 API,先说原理再举例。

底层源码:Pinia 的 store 是怎么实现的?

Pinia 的核心是用 reactive 包装 store 的 state,用 computed 实现 getters,用普通函数实现 actions(详见 Pinia 源码 packages/pinia/src/store.ts):
这段代码说明了 Pinia 的设计哲学:没有魔法,就是 Vue 原生的响应式 API。state 是 reactive,getters 是 computed,actions 是普通函数。Vuex 需要 mutations 来追踪变化,Pinia 直接用 Proxy 拦截 state 的修改,DevTools 一样可以追踪。 history 模式利用了浏览器的 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>。不要一次性重写所有组件。

常见追问

答题要点: mutations 的初衷是追踪状态变化,但实践中只是样板代码。高分回答: “Vuex 的 mutations 存在的初衷是追踪状态变化—每次修改 state 都通过 mutation,DevTools 可以记录每次变化。但实践中,mutations 只是样板代码:
Pinia 直接在 actions 中修改 state,DevTools 依然可以追踪变化(通过 Proxy 拦截)。这让代码更简洁,同时不丢失调试能力。”
答题要点: Pinia 的 getters 底层就是 computed,但有自动 this 绑定。高分回答: “Pinia 的 getters 底层实现就是 computed(packages/pinia/src/store.ts 中的 createSetupStore)。但 Pinia 做了一层封装:1)自动绑定 this 到当前 store,可以直接访问其他 getters 和 state;2)支持传参——返回一个函数而不是值;3)支持在其他 getters 中引用。
和直接在组件中用 computed 的区别是:Pinia 的 getters 是 store 级别的缓存,多个组件共享同一个 store 时,getter 只计算一次。”
答题要点: storeToRefs 把 store 的 state/getters 转为 ref,解构不丢失响应式。高分回答: “直接解构 Pinia store 会丢失响应式——因为解构是值复制,和 reactive 对象的解构问题一样。
storeToRefs(packages/pinia/src/store.ts)内部遍历 store 的 state 和 getters,把每个属性转为 ref。actions 不需要转换——actions 是普通函数,不涉及响应式。”
答题要点: hash 用 hashchange 事件,history 用 pushState + popstate。高分回答: “hash 模式的 URL 带 #(如 /#/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 中提取路径。”
答题要点: 三级守卫按顺序执行:组件离开 → 全局 → 路由 → 组件进入。高分回答: “导航守卫的完整执行顺序:
关键细节:onBeforeRouteEnter 的 next 回调中才能访问组件实例(next(vm => { ... })),因为此时组件还没有创建。onBeforeRouteLeave 常用于表单未保存确认。”
答题要点: 子路由渲染在父组件的 <router-view> 中,支持多层嵌套。高分回答: “嵌套路由通过在父组件中放置 <router-view> 来渲染子路由。Vue Router 的源码中(src/routerView.ts),<router-view> 组件会根据当前匹配的路由记录链(matched array),找到对应层级的组件来渲染。路由配置:
路由匹配时,Vue Router 会构建一个 matched 数组,包含从根路由到当前路由的所有记录。每个 <router-view> 根据自己的深度(depth)从 matched 数组中取出对应的组件来渲染。”
答题要点: 用动态 import() 实现,打包时自动分割代码。高分回答: “路由懒加载通过动态 import() 实现:
import() 返回一个 Promise。Vue Router 在匹配到该路由时,会调用这个函数加载组件。加载完成后,Vite/webpack 自动把该组件分割为单独的 chunk 文件。Vue Router 内部对懒加载做了优化:如果同一个路由组件被多个路由使用,会缓存 Promise 的结果,避免重复加载。在源码中(src/utils/resolveComponents.ts),resolveComponent 函数负责处理异步组件的加载和缓存。”
答题要点: Composables 显式返回值,Mixins 隐式合并。高分回答: “Mixins 是隐式合并—把 Mixin 的所有属性混入组件,你不知道某个属性来自哪个 Mixin,容易命名冲突。Composables 是显式调用—const { count, increment } = useCounter(),你一眼就知道 count 来自 useCounter。Composables 还支持 TypeScript 类型推断,Mixins 做不到。更深层的区别:Mixins 的生命周期钩子会和组件的钩子合并执行(Vue 2 在 mergeOptions 中合并),Composables 的生命周期钩子直接注册到组件实例上(在 setup() 中调用 onMounted 等)。Composables 之间可以相互组合(useSearch 内部调用 useDebounce),形成清晰的调用链。Mixins 之间如果有隐式依赖(一个 Mixin 访问了另一个 Mixin 的 this.xxx),这种关系完全隐藏在代码中,极难维护。”
答题要点: ref 嵌套过度、在非 setup 上下文中使用、忘记返回值。高分回答: “常见的错误用法包括:1)过度嵌套 ref: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。”
答题要点: setup() 在 beforeCreate 之前执行,比 created 更早。高分回答: “setup() 在 beforeCreate 之前执行,自然也比 created 更早。执行 setup() 时组件实例已经创建,但 setup 的签名不暴露实例代理,所以拿不到 this。这也是为什么 Composition API 用 ref()、reactive() 来声明状态,而不是用 data() 返回对象。Vue 3 的 Options API 中,created 钩子依然存在,执行时机不变。”
答题要点: Vue 3 支持混用,但不推荐,会导致代码风格不一致。高分回答: “Vue 3 支持在同一个组件中同时使用 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 用嵌套 module,Pinia 用扁平化的独立 store。高分回答: “Vuex 的模块化是嵌套的——一个大 store 下面嵌套多个 module,通过 namespaced: true 避免命名冲突。访问时用 store.state.user.name 或 mapState('user', ['name'])。嵌套层级深了之后,路径非常长,类型推断也很难做。Pinia 的模块化是扁平的——每个 store 是一个独立的实体,有自己的 id、state、getters、actions。访问时直接 const userStore = useUserStore()。store 之间可以相互引用:
Pinia 的扁平化设计让每个 store 独立可测试,不需要依赖整个 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 仍然更好?

  1. 团队已熟悉 Vuex:迁移成本大于收益
  2. 需要严格的变更追踪:mutations 的显式声明让状态变更有迹可循
  3. 大型团队协作:mutations 作为”变更契约”,团队成员必须遵守

什么时候 Pinia 更好?

  1. 新项目:Pinia 是 Vue 3 的官方推荐
  2. TypeScript 优先:Pinia 的类型推断更完善
  3. 简洁优先:没有 mutations 的样板代码
  4. 模块化需求:扁平化 store 更易维护

Q11: 简易实现 Vuex


Q12: 简易实现 Pinia

以上实现省略了:DevTools 集成、SSR 支持、热更新(HMR)、reset、reset、patch、storeToRefs 等功能。生产环境请使用官方 Pinia。

延伸阅读与引用参考