文章目录


一、Vue 2 (Options API) vs Vue 3 (Composition API) 深度对比

1. 核心定义

  • Options API (Vue 2):通过定义 data, methods, computed, watch 等选项来组织逻辑。
  • Composition API (Vue 3):通过 setup 函数,将相同逻辑功能的变量与函数组合在一起。

2. 优缺点详表

维度Options API (选项式)Composition API (组合式)
优点上手简单:结构固定,新手友好。
规范统一:代码看起来很整齐。
逻辑聚合:相同功能的代码可以写在一起。
复用性极强:通过 Hooks (Composables) 轻松复用。
TS 支持完美:原生支持类型推导。
Tree-shaking:按需引入,打包体积更小。
缺点逻辑碎片化:大型组件中,一个功能的代码散落在各处。
复用困境:Mixins 容易导致命名冲突和来源不明。
TS 适配差:难以进行类型推导。
学习成本:需要理解 ref, reactive, toRefs 等底层概念。
自由度过高:如果缺乏规范,代码可能写得比 Options API 还乱。

3. 为什么 Vue 3 最终选择了 Composition API?

A. 解决“逻辑碎片化”问题

在 Options API 中,处理“搜索”功能的代码可能在 data 里有变量,在 methods 里有方法,在 mounted 里有初始化。当组件变得庞大时,开发者需要频繁上下滚动。
Composition API 允许你把这部分逻辑提取到一个独立的函数中,实现“高内聚、低耦合”。

B. 彻底取代 Mixins

Vue 2 时代逻辑复用主要靠 Mixins,但它有三大痛点:

  1. 命名冲突:Mixins 的属性会被合并到组件中。如果两个 Mixin 甚至组件本身定义了同名的 data 或 methods,后引入的会覆盖前面的。
    • 痛点:当你引入多个 Mixin 时,你必须小心翼翼地检查每一个变量名,防止被覆盖。
  2. 来源不明:这是 Mixins 最致命的问题。在模板中使用一个变量时,如果它不是定义在当前组件里,你很难一眼看出它来自哪个 Mixin。
    • 痛点:当一个组件引入了 5 个 Mixin 时,调试代码就像在走迷宫,你需要在 5 个不同的文件中搜索某个函数的定义。
  3. 缺乏灵活性与高耦合:Mixins 很难接收参数来改变其内部逻辑。它们通常是“全量引入”,你无法按需取用。
    • Composition API 通过函数导入导出的方式,让数据来源变得清晰透明。

C. 拥抱 TypeScript

Options API 的 this 上下文在 TypeScript 中很难进行类型推导。而 Composition API 基于普通的变量和函数,天然适配 TS,极大地提升了大型项目的维护性。

D. 为什么组合式 API 能取代它?

组合式 API 通过函数式的思维解决了上述问题:

  1. 显式引入:变量和函数是通过解构赋值获取的,来源一目了然。

  2. 重命名机制:由于是解构赋值,你可以随意重命名变量,彻底告别命名冲突。

  3. 参数传递:你可以像调用普通函数一样传参,逻辑更加灵活。


4. 形象类比

  • Options API 就像是整理房间的抽屉:袜子放袜子抽屉,内衣放内衣抽屉。虽然整齐,但如果你要穿一套衣服,你得打开好几个抽屉。
  • Composition API 就像是搭配好的套装:把上衣、裤子、袜子配好放在一起。你要穿哪套,直接整套拿走就行。

💡 面试金句:
“Composition API 并不是要取代 Options API,它实际上是为解决复杂业务逻辑的组织与复用而生的。它让 Vue 具备了处理大型、长期维护项目的能力,同时也让 Vue 与 TypeScript 的结合达到了工业级的水准。”


二、Vue2 与 Vue3 生命周期对比及父子组件执行顺序

1. 生命周期钩子对比表

Vue3 引入了 Composition API,生命周期钩子改为了以 on 开头的函数,并移除了 beforeDestroydestroyed

阶段Vue2 (Options API)Vue3 (Composition API)作用及常见用途
创建beforeCreate- (用 setup 代替)实例初始化之后。
created- (用 setup 代替)实例创建完成,可访问数据。适合初始化非 DOM 数据
挂载beforeMountonBeforeMountDOM 挂载前。
mountedonMountedDOM 挂载完成。适合操作 DOM、发起网络请求
更新beforeUpdateonBeforeUpdate数据更新,DOM 重新渲染前。
updatedonUpdatedDOM 更新完成。避免在此修改状态(易死循环)
卸载beforeDestroyonBeforeUnmount实例销毁前。适合清除定时器、解绑事件
destroyedonUnmounted实例销毁后。
缓存activatedonActivated<keep-alive> 缓存的组件激活时调用。
deactivatedonDeactivated<keep-alive> 缓存的组件停用时调用。

2. 核心钩子能做些什么?

  • created / setup: 尽早发起网络请求(API Call),初始化不需要操作 DOM 的变量。
  • mounted: 涉及 DOM 操作的插件初始化(如:ECharts、Swiper、Map)。
  • beforeUnmount: 非常关键!必须在这里销毁定时器 (setInterval)、取消全局事件监听 (window.addEventListener),否则会导致内存泄漏
  • activated: 在被缓存的页面中,如果你希望用户每次切回该页面都刷新数据,请在此执行。

3. 父子组件生命周期执行顺序

Vue 的核心逻辑是:父组件先开始,子组件先结束;父组件先挂载,子组件先完成。

A. 初次挂载过程

  1. beforeCreate / setup
  2. created
  3. beforeMount
  4. beforeCreate / setup
  5. created
  6. beforeMount
  7. mounted (子组件先挂载完成)
  8. mounted (父组件最后挂载完成)

B. Keep-Alive 激活与停用

当父子组件都被 <keep-alive> 包裹时:
首次进入

  1. 父 beforeMount

  2. 子 beforeMount

  3. 子 mounted

  4. 子 activated

  5. 父 mounted

  6. 父 activated

离开组件 (切换到其他页面)

  1. 子 deactivated

  2. 父 deactivated

注意:即使是离开,也是父级先感知到失活。

再次进入 (命中缓存)

  1. 父 activated

  2. 子 activated

注意:命中缓存时,逻辑通常是从父到子依次激活。

C. 数据更新过程

  1. beforeUpdate
  2. beforeUpdate
  3. updated
  4. updated

D. 销毁过程

  1. beforeUnmount
  2. beforeUnmount
  3. unmounted
  4. unmounted

总结规律:父组件总是最先触发“开始”动作,但最后触发“完成”动作。子组件则包裹在父组件的生命周期之内执行。

三、 Vue 响应式原理深度剖析:Computed & Watch (Vue 2 vs Vue 3)

在 Vue 的响应式系统中,computedwatch 是开发者最常用的两个工具。从 Vue 2 到 Vue 3,虽然用法上保持了延续性,但底层实现从 Object.defineProperty 进化到了 Proxy


1. Vue 2:基于 Watcher 的派发更新

在 Vue 2 中,响应式的核心是 Dep 类和 Watcher 类。所有的响应式行为都由不同类型的 Watcher 驱动。

A Vue 2 中的三种 Watcher

Vue 2 内部统一使用 Watcher 类,通过不同的参数标识来区分功能:

类型描述
Render Watcher每个组件对应一个。当数据变化时,触发组件的 re-renderpatch
Computed Watcher为计算属性创建。具有 Lazy(惰性) 特性,只有在取值且依赖变化时才重新计算。
User Watcher开发者手动写的 watch。支持 deepimmediate 等配置。

B Computed 的底层原理(Vue 2)

  • 缓存机制:每个 Computed 属性对应一个 Watcher,内部有一个属性 dirty
  • 初次访问dirtytrue,执行求值函数,计算完成后将 dirty 置为 false
  • 依赖收集:计算属性执行时,会访问响应式数据,从而让 Computed Watcher 订阅该数据的 Dep
  • 依赖触发:当数据改变时,通知 Computed Watcher,它并不立即重新计算,而是将 dirty 设为 true。此时,它会通知 Render Watcher 重新渲染。渲染时再次读取 Computed,发现 dirtytrue,从而获取新值。

2. Vue 3:基于 Effect 的响应式系统

Vue 3 彻底重构了响应式,不再使用 Watcher 类,而是引入了更轻量、更强大的 Effect(副作用函数) 概念。

A Vue 3 中的三种 Effect

Vue 3 的底层核心是 ReactiveEffect 类,所有的响应式行为都是一个 Effect。

类型描述
Component Effect负责组件渲染。Vue 3 的渲染函数包裹在 effect 中。
Computed Effectcomputed() 返回的是一个只读的响应式 ref。内部封装了一个 effect
Watch Effect开发者使用的 watchwatchEffect

B Vue 3 底层源码剖析

(1)Computed 实现步骤
  1. 创建 Effect:调用 computed(getter) 时,内部会创建一个 ReactiveEffect 实例。
  2. 调度器(Scheduler):这是关键。Computed 的 Effect 拥有一个定制的调度器。当依赖变动时,不执行 getter,而是将 _dirty 设为 true,并触发 triggerRefValue(this)
  3. 取值逻辑
    • 访问 .value 时,触发 trackRefValue(收集依赖)。
    • 如果 _dirtytrue,执行 effect.run() 重新求值并关闭 _dirty
(2)Watch 实现步骤
  1. 标准化 Source:将输入的监视对象(ref, reactive, function)转化为一个 getter 函数。
  2. 定义 Job:创建一个调度任务 job,内部执行用户的 callback
  3. 创建 Effect:实例化 ReactiveEffect,并将 job 作为调度器。
  4. 执行
    • 如果是 immediate: true,立即执行。
    • 正常情况下,当依赖数据变化,调度器 job 被触发,执行回调。

3. Vue 2 vs Vue 3 深度对比

A 响应式劫持手段

  • Vue 2: 使用 Object.defineProperty。缺点是无法监听对象属性的添加/删除,以及数组索引的直接修改(需要特殊 Hack)。
  • Vue 3: 使用 ES6 Proxy。可以完美拦截对象的所有操作(deletehas 等)以及数组的所有变化,且性能更优(按需递归)。

B 依赖收集模型

  • Vue 2 (Dep-Watcher)
    D e p → [ W a t c h e r 1 , W a t c h e r 2 ] Dep \rightarrow [Watcher1, Watcher2] Dep[Watcher1,Watcher2]
    每个属性对应一个 Dep 实例,里面存储着订阅它的 Watcher
  • Vue 3 (WeakMap-Map-Set)
    使用全局统一的 targetMap 存储结构:
    WeakMap { target -> Map { key -> Set [effects] } }
    这种结构不需要在对象上挂载 Dep 实例,更加解耦。

4. 总结:如何选择 computed 和 watch?

维度Computed (计算属性)Watch (侦听器)
核心目的派生出新值,简化模板逻辑。执行副作用操作(API调用、DOM修改)。
缓存有缓存。只有依赖变了才会重算。无缓存。变化即执行。
异步不支持异步操作。支持异步操作。
底层实现惰性 Effect / Dirty 标记位。立即/调度 Effect。

💡 一句话建议:

如果一个值需要经过其他数据计算得出,用 computed;如果你需要在某个数据变化时去“干一件事”(如发请求、存本地存储),用 watch


Vue 2 和 Vue 3 虽然在底层实现上从“Watcher 模式”转向了“Effect 模式”,但其 依赖收集(Track)与派发更新(Trigger) 的核心思想是一脉相承的。

四、模板编译过程

从 Vue 2 到 Vue 3,模板编译的底层逻辑发生了巨大变化。简单来说,模板编译(Compile) 的过程就是将 HTML 字符串转换成一个返回虚拟 DOM(VNode)的 JavaScript 函数。

在 Vue 中,将 template 编译为 render 函数是提升运行时性能的关键。虽然 Vue 2 和 Vue 3 的基本思路一致(解析 -> 转化 -> 生成),但在实现细节和编译优化上,Vue 3 有了质的飞跃。


1. Vue 2 的编译过程:经典的“三步走”

Vue 2 的编译器(vue-template-compiler)逻辑相对线性:

A. 解析阶段 (Parse)

  • 利用正则匹配将模板字符串转换成 抽象语法树 (AST)
  • 识别指令(v-if, v-for)、属性、事件绑定。
  • 生成一个描述层级的 JavaScript 对象。

B. 优化阶段 (Optimize)

这是 Vue 2 特有的步骤。编译器会 深度遍历 AST,检测出其中的 静态节点 (Static Root)

  • 目的:如果一个节点及其子节点永远不会改变(如纯文本),Vue 会将其打上静态标记
  • 好处:在后续补丁(Patch)过程中,直接跳过这些静态树,减少对比开销。

C. 生成阶段 (Generate)

将 AST 转换成一段形式如 with(this){ return _c('div', { attrs: {"id": "app"} }, [...]) }渲染函数字符串,最后通过 new Function() 转化为真正的函数。


2. Vue 3 的编译过程:工业级的重构

Vue 3 的编译器(@vue/compiler-dom)变得模块化且异常强大,过程分为:

A. 解析 (Parse)

生成 AST。与 Vue 2 不同,Vue 3 的解析器更接近原生 HTML 解析逻辑,容错性更好。

B. 转换 (Transform)

这是 Vue 3 的核心。它不再只是简单地标记静态节点,而是进行 “预分析”

  • 静态提升 (Static Hoisting):将静态节点提升到渲染函数之外,只创建一次,重复使用。
  • 属性压缩:合并重复的类名、样式。

C. 生成 (Codegen)

生成高度优化的代码。Vue 3 舍弃了 with 语句,生成的代码更易于 JS 引擎优化(JIT 友好)。


3. Vue 3 相比 Vue 2 的重大优化

Vue 3 的编译优化被统称为 “编译时驱动的运行时优化”

A. PatchFlag (静态标记)

在 Vue 2 中,即使只有一个动态绑定,diff 算法也会全量对比整个组件树。
Vue 3 在生成的 render 函数中加入了 PatchFlag

  • createVNode 时,会给动态节点打上一个位掩码数字(例如 1 代表文本动态,2 代表 class 动态)。
  • 结果:diff 算法变成“靶向更新”,只对比有标记的属性,性能提升数倍。

B. HoistStatic (静态提升)

  • Vue 2:每次渲染都会重新创建静态节点的虚拟 DOM。
  • Vue 3:将不含变量的节点提升到 render 函数外部声明。
  • 结果:内存占用降低,多次渲染时直接复用同一个对象。

C. CacheHandlers (事件监听器缓存)

  • Vue 2:每次渲染都为元素重新绑定一个新的内联函数。
  • Vue 3:会自动缓存事件处理函数。
  • 结果:避免了因函数引用变化导致的子组件不必要更新。

D. Block Tree (块级树)

Vue 3 引入了 Block 的概念。在 v-ifv-for 之外的区域被视为一个稳定的“块”。

  • 结果:diff 算法从“深度遍历整个虚拟 DOM 树”进化为“遍历一个平坦的动态节点数组”,完全跳过了静态结构。

4. Vue 2 与 Vue 3 静态优化对比表

虽然 Vue 2 开启了静态优化的先河,但它存在明显的局限性,这也是 Vue 3 改进的重点:

优化维度Vue 2 的做法Vue 3 的做法
颗粒度“全家桶”式:标记逻辑以整个节点或子树为单位。要么整个子树是静态的(直接跳过),要么就是完全动态的(全量对比)。“靶向”式:利用 PatchFlag,即使节点是动态的,也能精准标记具体哪个属性(如:仅文本、仅类名或仅样式)是动态的,实现属性级别的更新。
内存占用每次组件重新渲染时,虽然 Patch 阶段会跳过对比,但依然会重复创建静态节点的虚拟 DOM 对象,造成不必要的内存开销。静态提升 (Hoisting):将静态节点及其属性定义移出渲染函数之外。全局只在初始化时创建一次对象,后续渲染直接复用引用,内存更加精简。
动态节点里的静态部分无法处理:如果一个 div 内部包含一个动态的 span,那么整个 div 及其子节点都无法被标记为静态,必须参与全量 Diff。Block Tree (块级树):引入“块”的概念。动态节点会被扁平化收集到 dynamicChildren 数组中。Diff 时只遍历该数组,完全忽略中间夹杂的静态结构。

总结对比

特性Vue 2Vue 3
Diff 方式全量 Diff (递归整个树)靶向 Diff (基于 PatchFlag)
静态内容仅在补丁阶段跳过静态提升,直接不参与重新创建
事件绑定每次渲染重新生成事件缓存 (Cache Handlers)
代码体积较难进行 Tree-shaking模块化设计,支持 Tree-shaking

Vue 3 的核心哲学是能在编译时确定的事情,绝不留到运行时去做。 这使得它在处理大型复杂应用时,性能表现远超 Vue 2。

五、 Vue3 核心升级深度解析

Vue3 并不是简单的版本升级,而是从底层架构到开发模式的一次全面重构。可以从设计理念、底层原理、开发模式、以及性能优化四个维度深入理解。


1. 响应式系统的升级(底层核心)

这是 Vue3 最重大的改变。Vue2 使用的是 Object.defineProperty,而 Vue3 改用了 ES6 的 Proxy

  • Vue2 的局限:
    • 无法检测到对象属性的新增或删除
    • 数组下标的直接修改和长度变化监听不到(需要特意重写数组方法)。
    • 深度嵌套的对象需要一次性递归到底,性能开销大。
  • Vue3 的优势:
    • 全方位监听: Proxy 可以直接监听整个对象而非属性,新增、删除属性都能自动触发更新。
    • 原生支持数组: 不需要额外对数组方法进行黑盒重写。
    • 惰性监听(Lazy Initialization): 只有当访问到某个嵌套属性时,才会对该属性进行响应式处理,大大提升了初始渲染性能。

2. 组合式 API (Composition API)

Vue2 采用的是 Options API(选项式),Vue3 引入了 Composition API

特性Vue2 (Options API)Vue3 (Composition API)
逻辑组织相同功能的代码被拆分在 data, methods, computed 中,散落在各处。相同功能的逻辑可以组合在一起,甚至抽离成独立的 useXxx 函数。
逻辑复用主要靠 Mixins。缺点:命名冲突、来源不明(隐式依赖)。Custom Hooks (Hooks 模式)。优点:来源清晰、类型推导友好、无命名冲突。
TS 支持对 TypeScript 支持较弱,需要复杂的配置。原生支持 TS,类型推导非常精准。

面试加分点: 提到 Composition API 解决了“长组件”逻辑碎片化的问题,极大地提高了大型项目的可维护性和代码的可读性。


3. 编译与性能优化

Vue3 在编译器层面做了大量的优化,使得渲染速度提升了 2~3 倍。

  1. 静态提升 (Static Hoisting):
    Vue2 中无论节点是否变化,每次渲染都会重新创建 VNode。Vue3 会将那些永远不会变的静态节点提升到渲染函数之外,只在初始化时创建一次。
  2. 补丁标记 (Patch Flag):
    Vue3 给包含动态内容的 VNode 加上了特定的标记(如:只有 class 变,或只有 text 变)。在 Diff 算法阶段,Vue3 只对比有标记的节点,不再全量对比。
  3. 缓存事件处理函数 (Cache Handlers):
    默认情况下,Vue 会认为绑定事件的函数是动态的(每次渲染都可能变)。Vue3 会将其缓存起来,避免不必要的组件子树更新。

4. 源码体积与构建 (Tree-shaking)

  • 按需编译: Vue2 的很多全局 API 是直接挂载在原型上的,即使不用,打包时也会包含在内。
  • Tree-shaking 支持: Vue3 进行了重构,将 API 模块化。如果你没用到 watchcomputed,打包工具(如 Vite/Webpack)就会自动剔除这些代码。这意味着最终生成的 Bundle 体积更小。

5. 其他新特性与内置组件

Vue3 引入了几个解决开发痛点的内置组件:

  • Fragment(碎片): 支持多个根节点,减少了不必要的 DOM 层级(不用再为了满足 Vue2 规则强行套一层 div)。
  • Teleport(传送门): 可以将子组件(如 Modal 弹窗)挂载到 body 或其他指定的 DOM 节点上,解决了 CSS 样式嵌套导致的层级/遮挡问题。
  • Suspense: 处理异步组件的加载状态,提供类似 Loading 的优雅过渡方案。

💡 总结:Vue3 面试回答口诀 (SOP)

  1. 底层看 Proxy: 解决了 Vue2 响应式的各种“坑”,监听更全面,性能更好。
  2. 模式看 Composition: 解决了逻辑复用难、代码维护难的问题,TS 支持更完美。
  3. 编译看静态提升: 通过 Patch Flag 等手段让 Diff 算法变“聪明”了,非全量对比。
  4. 工程看 Tree-shaking: 模块化设计让体积更轻量。

更多推荐