1. 关于 Pinia 和 Vuex 的区别,我认为核心主要有三点:
- 去掉了 Mutation,Pinia 的 Actions 既能处理同步也能处理异步,代码更简洁;
- 更好的 TypeScript 支持,Pinia 对类型推导非常友好,而 Vuex 写类型定义很痛苦;
- 扁平化架构,Vuex 是单树嵌套模块,容易产生命名冲突和路径过长,而 Pinia 每个文件都是独立的 Store,天然隔离,更符合组合式 API 的思维。
所以在新项目中,我首选 Pinia。关于持久化,通常我们会使用 pinia-plugin-persistedstate 插件,将 State 自动同步到 LocalStorage,解决刷新丢失数据的问题。
2. Vue 3 中组件通信的方式有哪些?
在 Vue 3 中,组件通信的方式可以按照组件关系进行分类,主要有以下几种:
一、父子组件通信
props/defineProps(父传子):父组件通过属性将数据传递给子组件,是最常用的单向数据流方式。emits/defineEmits(子传父):子组件通过派发自定义事件,将数据或状态变动通知给父组件。v-model(双向绑定):父子组件数据同步。Vue 3 支持给组件绑定多个v-model,而在 Vue 3.4+ 中更是引入了极简的defineModel宏。ref/defineExpose(父访子):父组件通过ref获取子组件实例,从而调用子组件的方法或访问数据(注意<script setup>下子组件默认封闭,需使用defineExpose显式暴露)。slots(插槽):主要用于模板内容的传递,包含默认插槽、具名插槽和作用域插槽(子传父数据)。
二、跨级(祖孙)组件通信
provide/inject(依赖注入):祖先组件通过provide提供数据,后代组件无论嵌套多深,都可以直接通过inject注入并使用。这有效避免了 Props 逐级透传(Props Drilling)的繁琐问题。
三、兄弟 / 全局(任意)组件通信
- 全局状态管理(Pinia / Vuex):官方现推荐使用 Pinia,提供全局响应式状态中心,任意组件均可读写,适合大型项目。
- Event Bus 事件总线(如第三方库
mitt):Vue 3 从实例中移除了$on、$off等方法,不再支持原生的 Event Bus。若有轻量级的全局事件触发需求,官方推荐引入第三方库(如mitt或tiny-emitter)实现发布-订阅模式。
四、路由通信 (Vue Router)
query参数:通过 URL 查询字符串传递,如/user?id=1。参数会显示在地址栏,且刷新不丢失,适用于搜索、过滤等场景。params动态路径参数:通过路径占位符传递,如/user/:id。参数嵌入路径中,结构化更强,常用于详情页跳转。props接收模式:在路由配置中开启props: true,可以将路由参数(query或params)直接映射为组件的props。这是官方推荐的解耦方式,让组件不依赖$route对象。
五、其他(补充项)
attrs/useAttrs(透传 Attributes):用于接收父组件传递的、但没有在子组件props或emits中声明过的属性和事件,常用于封装高级组件时透传原生的class、style、id或原生事件等。
3. Vue 3 中的 ref 和 reactive 有什么区别?该如何选择?
这是 Vue 3 开发中最基础也最核心的问题。它们的区别主要体现在数据类型、访问方式、以及使用限制上:
一、核心区别
- 数据类型支持:
ref:支持任意类型。既可以定义基本类型(String, Number, Boolean 等),也可以定义复杂类型(Object, Array)。reactive:仅支持复杂类型(Object, Array, Set, Map)。报错或无法生效若传入基本类型。
- 访问方式:
ref:在 JS/TS 中必须通过.value访问和修改,但在模板(<template>)中会被自动解包,无需.value。reactive:直接像访问普通对象一样访问,无需.value。
- 响应式实现:
ref:底层通常是通过RefImpl类包装,并使用Object.defineProperty的get/set(针对基本类型)或内部调用reactive(针对对象)来实现。reactive:底层直接使用 ES6 的 Proxy 对整个对象进行拦截。
二、使用限制(避坑指南)
reactive的“解构”丢失问题:- 如果对
reactive对象进行直接解构(如const { name } = reactiveObj),或者将该对象重新赋值(如reactiveObj = { ... }),会丢失其响应性。 - 解决办法:使用
toRefs进行解构,或者干脆使用ref定义。
- 如果对
ref的“冗余”问题:- 过多的
.value可能导致代码看起来比较繁琐(虽然有插件可以自动补全)。
- 过多的
三、开发建议(我的原则)
- 绝大多数场景首选
ref:它的心智负担更低,支持所有类型,且通过.value这种显式的操作,代码追踪更清晰。 - 只有当管理“高度聚合的业务数据”时建议用
reactive:例如表单数据对象(formState),将相关的字段放在一起,逻辑上更紧凑。 - 永远不要重新赋值
reactive对象,如果需要替换整个对象,请使用Object.assign(target, source)。
4. Vue 3 的生命周期钩子有哪些?与 Vue 2 相比有什么变化?
在 Vue 3 的 Composition API 中,生命周期钩子的使用方式发生了变化,且为了更符合组合式 API 的习惯,大多数钩子都加上了 on 前缀。
一、Vue 2 与 Vue 3 生命周期对比表
| 生命周期阶段 | Vue 2 (Options API) | Vue 3 (Composition API) |
|---|---|---|
| 创建前/后 | beforeCreate / created | 不需要 (直接写在 setup 或 <script setup> 中) |
| 挂载前/后 | beforeMount / mounted | onBeforeMount / onMounted |
| 更新前/后 | beforeUpdate / updated | onBeforeUpdate / onUpdated |
| 销毁前/后 | beforeDestroy / destroyed | onBeforeUnmount / onUnmounted |
| 异常捕获 | errorCaptured | onErrorCaptured |
二、核心变化点
- 去掉了
beforeCreate和created:在 Vue 3 中,setup函数执行的时机就在这两个钩子之间。因此,原本写在created里的初始化逻辑(如发起网络请求),现在直接写在setup顶层即可。 - 销毁钩子重命名:
beforeDestroy改为了onBeforeUnmount,destroyed改为了onUnmounted。语义上更加准确,强调整体是从 DOM 上“卸载”组件的过程。 - 新增调试钩子:Vue 3 提供了
onRenderTracked和onRenderTriggered,方便开发者追踪响应式依赖,定位渲染性能瓶颈。
三、使用注意事项
- 必须同步调用:生命周期钩子(如
onMounted)必须是在setup期间同步注册。不能放在setTimeout或Promise.then等异步回调中注册,否则钩子将无法绑定到当前的组件实例上。 - 多次调用:在同一个组件中,你可以多次调用同一个钩子(如写两个
onMounted),它们会按照代码书写的顺序依次执行。这在逻辑复用(Hooks 提取)时非常有用。
5. v-if 和 v-show 有什么区别?
这是 Vue 中最基础的指令对比,区别主要在于渲染机制和性能开销:
一、核心区别
- 渲染方式:
v-if:是“真正”的条件渲染。它会确保在切换过程中,条件块内的事件监听器和子组件适当地被销毁和重建。如果初始条件为假,则什么也不做。v-show:不管初始条件是什么,元素总是会被渲染,并且只是简单地通过 CSS 的display: none属性进行切换。
- 性能开销:
v-if:有更高的切换消耗。因为它涉及 DOM 树的增删,如果频繁切换,开销较大。v-show:有更高的初始渲染消耗。因为它无论如何都会渲染,只是隐藏了。
二、使用场景建议
- 用
v-if:如果运行时条件很少改变,或者切换频率非常低。 - 用
v-show:如果需要非常频繁地切换(如:控制弹窗显隐、侧边栏切换、Tab 切换等)。
TIP
组合使用注意:不建议在同一个元素上同时使用 v-if 和 v-for。在 Vue 3 中,v-if 的优先级高于 v-for,这会导致 v-if 无法访问 v-for 循环中的变量。建议将 v-if 移动至外层容器(如 template 标签)或使用 computed 过滤列表。
6. 谈谈 Vue 中的 nextTick 的原理和使用场景?
在 Vue 中,数据的更新是响应式的,但 DOM 的更新是异步的。这意味着当你修改了数据,DOM 并不会立刻变化,而是缓冲在一个队列中。
一、为什么需要 nextTick?
Vue 开启了一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种去重缓冲机制可以避免不必要的计算和 DOM 操作,从而提高性能。
当你需要在修改数据后,立刻获取更新后的 DOM 状态(位置、高度等)时,就需要用到 nextTick。
二、实现原理
nextTick 的核心是利用 JavaScript 的异步任务队列。它会尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果都不支持,则采用 setTimeout(fn, 0)。 通过这些微任务或宏任务,确保回调函数在当前的同步任务执行完毕、DOM 渲染队列清空之后再被调用。
三、使用场景
- 获取更新后的 DOM 属性:比如在列表新增一项后,滚动到最底部。
- 在
created钩子中操作 DOM:虽然不推荐,但如果要操作,必须包在nextTick里(因为created时 DOM 还没挂载)。
7. Composition API 与 Options API 有什么区别?
这是 Vue 2 到 Vue 3 最直观的变化,主要区别在于逻辑组织和复用能力:
一、Options API (Vue 2 风格)
- 特点:代码按照
data,methods,computed,watch等选项进行拆分。 - 优点:初学者容易上手,代码结构固定,类似于“填空题”。
- 缺点:当组件逻辑变得复杂时,同一个功能逻辑会被分散在不同的选项中,导致反复上下滚动。同时,逻辑复用(如
mixins)容易产生命名冲突和来源不明确等隐患。
二、Composition API (Vue 3 风格)
- 特点:代码按照“逻辑功能”进行组织,所有的响应式变量、函数、生命周期等都放在
setup内部。 - 优点:
- 逻辑聚合:可以将同一个功能的代码提取到一个独立的 Hook 函数(如
useUser.js)中。 - Tree-shaking 友好:API 都是按需引入的函数,减小了打包体积。
- 更强的类型推导:对 TypeScript 支持极佳。
- 逻辑聚合:可以将同一个功能的代码提取到一个独立的 Hook 函数(如
- 总结建议:在小型项目或简单组件中,Options API 依旧直观高效;但在大型复杂业务或团队协作中,Composition API 是维护性的保障。
8. 为什么 v-for 中一定要绑定 key?且不建议用 index?
key 是 Vue 中虚拟 DOM 的一个唯一标识,它的存在是为了高效地更新虚拟 DOM。
一、key 的作用
- 辅助 Diff 算法:在 Vue 的补丁过程(patch)中,
key是判断两个节点是否为同一个节点的首要条件。有了key,Vue 就可以精准地找到旧节点进行复用,而不是直接销毁重建。 - 维持组件状态:例如在使用
v-for渲染一组输入框时,如果没有key或key不唯一,当列表顺序改变时,输入框内的内容(非响应式状态)可能会发生错乱。
二、为什么不建议用 index 作为 key?
- 性能问题:如果我们在列表头部或中部插入/删除元素,那么该索引之后的所有元素的
index都会发生改变。这会导致 Vue 认为这些节点都发生了变化,从而触发大量不必要的 DOM 更新,失去了 Diff 算法的优势。 - 渲染 Bug:如果列表项包含复选框、输入框等具有临时状态的元素,使用
index作为key会导致状态跟错位置(例如:你删除了第一项,结果第二项的勾选状态跑到了原来第二项的位置,但实际上该项应该消失)。
三、最佳实践
- 始终使用数据库中的
id或其他具有唯一性的业务字段作为key。 - 只有在列表仅用于纯展示、且永远不会发生过滤、排序、增删等改变顺序的操作时,才勉强可以使用
index。
9. Vue 2 和 Vue 3 的核心区别有哪些?
Vue 3 相比 Vue 2 的整体变化,可以分为以下四大类:
一、源码优化
- TypeScript 重构:Vue 3 使用 TypeScript 重构了整个源码,提供了更好的类型推导。
- Monorepo 管理:源码结构转为 Monorepo 模式,模块划分粒度更细,各包可以独立测试和发布。
- 按需引入:用户可以根据需要单独引入某个包(如
@vue/reactivity),而不是必须引入整个 Vue 框架。 - 精简体积:移除了某些冷门功能(如
filter、inline-template等),由更加现代化的方式替代。
二、性能优化
Vue 3 的性能相比 Vue 2 有质的飞跃,基本上将性能做到了极致。
- 响应式系统:由
Object.defineProperty改为 ES6 Proxy,解决了无法监听属性增删及数组变化的痛点,且性能更佳。 - Diff 算法优化:引入了 Patch Flag(静态标记) 和 Hoisting(静态提升),大幅减少了不必要的 DOM 对比。
- 模板编译:通过更智能的编译策略,显著提升了运行时性能。
三、语法 API 优化(Composition API)
这是开发者感知最明显的变化。
- 逻辑组织:由 Options API 转向 Composition API。代码逻辑不再按
data、methods等选项拆分,而是按功能聚合,极大地提升了复杂代码的阅读和维护体验。 - 逻辑复用:以前推荐使用
mixin方案,容易产生隐患;Vue 3 推荐使用组合式函数(Composables),复用粒度更细且来源明确。 - 兼容性:Vue 3 并没有废弃 Options API,而是将其作为一种可选的编码风格保留。
四、引入 RFC 流程
- 规范化变更:尤雨溪和核心团队广泛采用了 RFC (Request for Comments) 流程。所有的重大更改和新功能都会经过社区讨论和共识,这使得框架的发展更加透明和稳健。
TIP
面试小贴士:如果面试官追问“性能优化”,你可以从响应式拦截、模板静态提升、源码 Tree-shaking 这三个点深入展开;如果是追问“开发体验”,则重点突出 Composition API 和 TS 支持。
10. 介绍一下 Vue 3 内部的运行机制是怎样的?
Vue 3 是一个声明式的框架。声明式的好处在于,它直接描述结果,用户不需要关注过程。
一、描述 UI 的方式
Vue.js 主要采用 模板(Template) 的方式来描述 UI,但它同样支持使用 虚拟 DOM(VNode)。
- 模板:更加直观,易于阅读和维护。
- 虚拟 DOM:比模板更加灵活,能够处理复杂的逻辑场景。
二、核心模块及其协作流程
- 编译器 (Compiler):
- 当用户使用模板描述 UI 时,编译器会将其编译为渲染函数(Render Function)。
- 渲染函数执行后,能够确定响应式数据与渲染函数之间的依赖关系。
- 响应式系统 (Reactivity System):
- 一旦响应式数据发生变化,依赖该数据的渲染函数就会被触发重新执行。
- 渲染器 (Renderer):
- 渲染函数执行的结果是得到虚拟 DOM 对象。
- 渲染器负责将虚拟 DOM 渲染为真实 DOM 元素。
- 首次渲染:递归遍历虚拟 DOM 对象,调用原生 DOM API 创建真实元素。
- 更新渲染(Diff):这是渲染器的精髓。它会通过 Diff 算法 找出新旧虚拟 DOM 的变更点,并只更新需要变动的内容。
三、总结
编译器、渲染器、响应式系统是 Vue 内部的核心模块。它们共同构成一个有机的整体,不同模块之间互相配合,从编译时优化到运行时的高效更新,全方位提升了框架的性能。
