Skip to content

1. 关于 Pinia 和 Vuex 的区别,我认为核心主要有三点:

  1. 去掉了 Mutation,Pinia 的 Actions 既能处理同步也能处理异步,代码更简洁;
  2. 更好的 TypeScript 支持,Pinia 对类型推导非常友好,而 Vuex 写类型定义很痛苦;
  3. 扁平化架构,Vuex 是单树嵌套模块,容易产生命名冲突和路径过长,而 Pinia 每个文件都是独立的 Store,天然隔离,更符合组合式 API 的思维。

所以在新项目中,我首选 Pinia。关于持久化,通常我们会使用 pinia-plugin-persistedstate 插件,将 State 自动同步到 LocalStorage,解决刷新丢失数据的问题。

2. Vue 3 中组件通信的方式有哪些?

在 Vue 3 中,组件通信的方式可以按照组件关系进行分类,主要有以下几种:

一、父子组件通信

  1. props / defineProps(父传子):父组件通过属性将数据传递给子组件,是最常用的单向数据流方式。
  2. emits / defineEmits(子传父):子组件通过派发自定义事件,将数据或状态变动通知给父组件。
  3. v-model(双向绑定):父子组件数据同步。Vue 3 支持给组件绑定多个 v-model,而在 Vue 3.4+ 中更是引入了极简的 defineModel 宏。
  4. ref / defineExpose(父访子):父组件通过 ref 获取子组件实例,从而调用子组件的方法或访问数据(注意 <script setup> 下子组件默认封闭,需使用 defineExpose 显式暴露)。
  5. slots(插槽):主要用于模板内容的传递,包含默认插槽、具名插槽和作用域插槽(子传父数据)。

二、跨级(祖孙)组件通信

  1. provide / inject(依赖注入):祖先组件通过 provide 提供数据,后代组件无论嵌套多深,都可以直接通过 inject 注入并使用。这有效避免了 Props 逐级透传(Props Drilling)的繁琐问题。

三、兄弟 / 全局(任意)组件通信

  1. 全局状态管理(Pinia / Vuex):官方现推荐使用 Pinia,提供全局响应式状态中心,任意组件均可读写,适合大型项目。
  2. Event Bus 事件总线(如第三方库 mitt:Vue 3 从实例中移除了 $on$off 等方法,不再支持原生的 Event Bus。若有轻量级的全局事件触发需求,官方推荐引入第三方库(如 mitttiny-emitter)实现发布-订阅模式。

四、路由通信 (Vue Router)

  1. query 参数:通过 URL 查询字符串传递,如 /user?id=1。参数会显示在地址栏,且刷新不丢失,适用于搜索、过滤等场景。
  2. params 动态路径参数:通过路径占位符传递,如 /user/:id。参数嵌入路径中,结构化更强,常用于详情页跳转。
  3. props 接收模式:在路由配置中开启 props: true,可以将路由参数(queryparams)直接映射为组件的 props。这是官方推荐的解耦方式,让组件不依赖 $route 对象。

五、其他(补充项)

  1. attrs / useAttrs(透传 Attributes):用于接收父组件传递的、但没有在子组件 propsemits 中声明过的属性和事件,常用于封装高级组件时透传原生的 classstyleid 或原生事件等。

3. Vue 3 中的 refreactive 有什么区别?该如何选择?

这是 Vue 3 开发中最基础也最核心的问题。它们的区别主要体现在数据类型、访问方式、以及使用限制上:

一、核心区别

  1. 数据类型支持
    • ref:支持任意类型。既可以定义基本类型(String, Number, Boolean 等),也可以定义复杂类型(Object, Array)。
    • reactive仅支持复杂类型(Object, Array, Set, Map)。报错或无法生效若传入基本类型。
  2. 访问方式
    • ref:在 JS/TS 中必须通过 .value 访问和修改,但在模板(<template>)中会被自动解包,无需 .value
    • reactive:直接像访问普通对象一样访问,无需 .value
  3. 响应式实现
    • ref:底层通常是通过 RefImpl 类包装,并使用 Object.definePropertyget/set(针对基本类型)或内部调用 reactive(针对对象)来实现。
    • reactive:底层直接使用 ES6 的 Proxy 对整个对象进行拦截。

二、使用限制(避坑指南)

  1. reactive 的“解构”丢失问题
    • 如果对 reactive 对象进行直接解构(如 const { name } = reactiveObj),或者将该对象重新赋值(如 reactiveObj = { ... }),会丢失其响应性。
    • 解决办法:使用 toRefs 进行解构,或者干脆使用 ref 定义。
  2. 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 / mountedonBeforeMount / onMounted
更新前/后beforeUpdate / updatedonBeforeUpdate / onUpdated
销毁前/后beforeDestroy / destroyedonBeforeUnmount / onUnmounted
异常捕获errorCapturedonErrorCaptured

二、核心变化点

  1. 去掉了 beforeCreatecreated:在 Vue 3 中,setup 函数执行的时机就在这两个钩子之间。因此,原本写在 created 里的初始化逻辑(如发起网络请求),现在直接写在 setup 顶层即可。
  2. 销毁钩子重命名beforeDestroy 改为了 onBeforeUnmountdestroyed 改为了 onUnmounted。语义上更加准确,强调整体是从 DOM 上“卸载”组件的过程。
  3. 新增调试钩子:Vue 3 提供了 onRenderTrackedonRenderTriggered,方便开发者追踪响应式依赖,定位渲染性能瓶颈。

三、使用注意事项

  • 必须同步调用:生命周期钩子(如 onMounted)必须是在 setup 期间同步注册。不能放在 setTimeoutPromise.then 等异步回调中注册,否则钩子将无法绑定到当前的组件实例上。
  • 多次调用:在同一个组件中,你可以多次调用同一个钩子(如写两个 onMounted),它们会按照代码书写的顺序依次执行。这在逻辑复用(Hooks 提取)时非常有用。

5. v-ifv-show 有什么区别?

这是 Vue 中最基础的指令对比,区别主要在于渲染机制性能开销

一、核心区别

  1. 渲染方式
    • v-if:是“真正”的条件渲染。它会确保在切换过程中,条件块内的事件监听器和子组件适当地被销毁和重建。如果初始条件为假,则什么也不做。
    • v-show:不管初始条件是什么,元素总是会被渲染,并且只是简单地通过 CSS 的 display: none 属性进行切换。
  2. 性能开销
    • v-if:有更高的切换消耗。因为它涉及 DOM 树的增删,如果频繁切换,开销较大。
    • v-show:有更高的初始渲染消耗。因为它无论如何都会渲染,只是隐藏了。

二、使用场景建议

  • v-if:如果运行时条件很少改变,或者切换频率非常低。
  • v-show:如果需要非常频繁地切换(如:控制弹窗显隐、侧边栏切换、Tab 切换等)。

TIP

组合使用注意:不建议在同一个元素上同时使用 v-ifv-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.thenMutationObserversetImmediate,如果都不支持,则采用 setTimeout(fn, 0)。 通过这些微任务或宏任务,确保回调函数在当前的同步任务执行完毕、DOM 渲染队列清空之后再被调用。

三、使用场景

  1. 获取更新后的 DOM 属性:比如在列表新增一项后,滚动到最底部。
  2. 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 支持极佳。
  • 总结建议:在小型项目或简单组件中,Options API 依旧直观高效;但在大型复杂业务或团队协作中,Composition API 是维护性的保障。

8. 为什么 v-for 中一定要绑定 key?且不建议用 index

key 是 Vue 中虚拟 DOM 的一个唯一标识,它的存在是为了高效地更新虚拟 DOM。

一、key 的作用

  1. 辅助 Diff 算法:在 Vue 的补丁过程(patch)中,key 是判断两个节点是否为同一个节点的首要条件。有了 key,Vue 就可以精准地找到旧节点进行复用,而不是直接销毁重建。
  2. 维持组件状态:例如在使用 v-for 渲染一组输入框时,如果没有 keykey 不唯一,当列表顺序改变时,输入框内的内容(非响应式状态)可能会发生错乱。

二、为什么不建议用 index 作为 key

  1. 性能问题:如果我们在列表头部或中部插入/删除元素,那么该索引之后的所有元素的 index 都会发生改变。这会导致 Vue 认为这些节点都发生了变化,从而触发大量不必要的 DOM 更新,失去了 Diff 算法的优势。
  2. 渲染 Bug:如果列表项包含复选框、输入框等具有临时状态的元素,使用 index 作为 key 会导致状态跟错位置(例如:你删除了第一项,结果第二项的勾选状态跑到了原来第二项的位置,但实际上该项应该消失)。

三、最佳实践

  • 始终使用数据库中的 id 或其他具有唯一性的业务字段作为 key
  • 只有在列表仅用于纯展示、且永远不会发生过滤、排序、增删等改变顺序的操作时,才勉强可以使用 index

9. Vue 2 和 Vue 3 的核心区别有哪些?

Vue 3 相比 Vue 2 的整体变化,可以分为以下四大类:

一、源码优化

  • TypeScript 重构:Vue 3 使用 TypeScript 重构了整个源码,提供了更好的类型推导。
  • Monorepo 管理:源码结构转为 Monorepo 模式,模块划分粒度更细,各包可以独立测试和发布。
  • 按需引入:用户可以根据需要单独引入某个包(如 @vue/reactivity),而不是必须引入整个 Vue 框架。
  • 精简体积:移除了某些冷门功能(如 filterinline-template 等),由更加现代化的方式替代。

二、性能优化

Vue 3 的性能相比 Vue 2 有质的飞跃,基本上将性能做到了极致。

  • 响应式系统:由 Object.defineProperty 改为 ES6 Proxy,解决了无法监听属性增删及数组变化的痛点,且性能更佳。
  • Diff 算法优化:引入了 Patch Flag(静态标记)Hoisting(静态提升),大幅减少了不必要的 DOM 对比。
  • 模板编译:通过更智能的编译策略,显著提升了运行时性能。

三、语法 API 优化(Composition API)

这是开发者感知最明显的变化。

  • 逻辑组织:由 Options API 转向 Composition API。代码逻辑不再按 datamethods 等选项拆分,而是按功能聚合,极大地提升了复杂代码的阅读和维护体验。
  • 逻辑复用:以前推荐使用 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:比模板更加灵活,能够处理复杂的逻辑场景。

二、核心模块及其协作流程

  1. 编译器 (Compiler)
    • 当用户使用模板描述 UI 时,编译器会将其编译为渲染函数(Render Function)
    • 渲染函数执行后,能够确定响应式数据渲染函数之间的依赖关系。
  2. 响应式系统 (Reactivity System)
    • 一旦响应式数据发生变化,依赖该数据的渲染函数就会被触发重新执行。
  3. 渲染器 (Renderer)
    • 渲染函数执行的结果是得到虚拟 DOM 对象
    • 渲染器负责将虚拟 DOM 渲染为真实 DOM 元素
    • 首次渲染:递归遍历虚拟 DOM 对象,调用原生 DOM API 创建真实元素。
    • 更新渲染(Diff):这是渲染器的精髓。它会通过 Diff 算法 找出新旧虚拟 DOM 的变更点,并只更新需要变动的内容。

三、总结

编译器、渲染器、响应式系统是 Vue 内部的核心模块。它们共同构成一个有机的整体,不同模块之间互相配合,从编译时优化到运行时的高效更新,全方位提升了框架的性能。