Vue2 vs Vue3:选项式与组合式API相关面试题、生命周期对比及父子组件执行顺序、Computed & Watch
文章目录
一、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,但它有三大痛点:
- 命名冲突:Mixins 的属性会被合并到组件中。如果两个 Mixin 甚至组件本身定义了同名的 data 或 methods,后引入的会覆盖前面的。
- 痛点:当你引入多个 Mixin 时,你必须小心翼翼地检查每一个变量名,防止被覆盖。
- 来源不明:这是 Mixins 最致命的问题。在模板中使用一个变量时,如果它不是定义在当前组件里,你很难一眼看出它来自哪个 Mixin。
- 痛点:当一个组件引入了 5 个 Mixin 时,调试代码就像在走迷宫,你需要在 5 个不同的文件中搜索某个函数的定义。
- 缺乏灵活性与高耦合:Mixins 很难接收参数来改变其内部逻辑。它们通常是“全量引入”,你无法按需取用。
- Composition API 通过函数导入导出的方式,让数据来源变得清晰透明。
C. 拥抱 TypeScript
Options API 的 this 上下文在 TypeScript 中很难进行类型推导。而 Composition API 基于普通的变量和函数,天然适配 TS,极大地提升了大型项目的维护性。
D. 为什么组合式 API 能取代它?
组合式 API 通过函数式的思维解决了上述问题:
-
显式引入:变量和函数是通过解构赋值获取的,来源一目了然。
-
重命名机制:由于是解构赋值,你可以随意重命名变量,彻底告别命名冲突。
-
参数传递:你可以像调用普通函数一样传参,逻辑更加灵活。
4. 形象类比
- Options API 就像是整理房间的抽屉:袜子放袜子抽屉,内衣放内衣抽屉。虽然整齐,但如果你要穿一套衣服,你得打开好几个抽屉。
- Composition API 就像是搭配好的套装:把上衣、裤子、袜子配好放在一起。你要穿哪套,直接整套拿走就行。
💡 面试金句:
“Composition API 并不是要取代 Options API,它实际上是为解决复杂业务逻辑的组织与复用而生的。它让 Vue 具备了处理大型、长期维护项目的能力,同时也让 Vue 与 TypeScript 的结合达到了工业级的水准。”
二、Vue2 与 Vue3 生命周期对比及父子组件执行顺序
1. 生命周期钩子对比表
Vue3 引入了 Composition API,生命周期钩子改为了以 on 开头的函数,并移除了 beforeDestroy 和 destroyed。
| 阶段 | Vue2 (Options API) | Vue3 (Composition API) | 作用及常见用途 |
|---|---|---|---|
| 创建 | beforeCreate |
- (用 setup 代替) |
实例初始化之后。 |
created |
- (用 setup 代替) |
实例创建完成,可访问数据。适合初始化非 DOM 数据。 | |
| 挂载 | beforeMount |
onBeforeMount |
DOM 挂载前。 |
mounted |
onMounted |
DOM 挂载完成。适合操作 DOM、发起网络请求。 | |
| 更新 | beforeUpdate |
onBeforeUpdate |
数据更新,DOM 重新渲染前。 |
updated |
onUpdated |
DOM 更新完成。避免在此修改状态(易死循环)。 | |
| 卸载 | beforeDestroy |
onBeforeUnmount |
实例销毁前。适合清除定时器、解绑事件。 |
destroyed |
onUnmounted |
实例销毁后。 | |
| 缓存 | activated |
onActivated |
被 <keep-alive> 缓存的组件激活时调用。 |
deactivated |
onDeactivated |
被 <keep-alive> 缓存的组件停用时调用。 |
2. 核心钩子能做些什么?
created/setup: 尽早发起网络请求(API Call),初始化不需要操作 DOM 的变量。mounted: 涉及 DOM 操作的插件初始化(如:ECharts、Swiper、Map)。beforeUnmount: 非常关键!必须在这里销毁定时器 (setInterval)、取消全局事件监听 (window.addEventListener),否则会导致内存泄漏。activated: 在被缓存的页面中,如果你希望用户每次切回该页面都刷新数据,请在此执行。
3. 父子组件生命周期执行顺序
Vue 的核心逻辑是:父组件先开始,子组件先结束;父组件先挂载,子组件先完成。
A. 初次挂载过程
- 父
beforeCreate/setup - 父
created - 父
beforeMount - 子
beforeCreate/setup - 子
created - 子
beforeMount - 子
mounted(子组件先挂载完成) - 父
mounted(父组件最后挂载完成)
B. Keep-Alive 激活与停用
当父子组件都被 <keep-alive> 包裹时:
首次进入
-
父 beforeMount
-
子 beforeMount
-
子 mounted
-
子 activated
-
父 mounted
-
父 activated
离开组件 (切换到其他页面)
-
子 deactivated
-
父 deactivated
注意:即使是离开,也是父级先感知到失活。
再次进入 (命中缓存)
-
父 activated
-
子 activated
注意:命中缓存时,逻辑通常是从父到子依次激活。
C. 数据更新过程
- 父
beforeUpdate - 子
beforeUpdate - 子
updated - 父
updated
D. 销毁过程
- 父
beforeUnmount - 子
beforeUnmount - 子
unmounted - 父
unmounted
总结规律:父组件总是最先触发“开始”动作,但最后触发“完成”动作。子组件则包裹在父组件的生命周期之内执行。
三、 Vue 响应式原理深度剖析:Computed & Watch (Vue 2 vs Vue 3)
在 Vue 的响应式系统中,computed 和 watch 是开发者最常用的两个工具。从 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-render 和 patch。 |
| Computed Watcher | 为计算属性创建。具有 Lazy(惰性) 特性,只有在取值且依赖变化时才重新计算。 |
| User Watcher | 开发者手动写的 watch。支持 deep、immediate 等配置。 |
B Computed 的底层原理(Vue 2)
- 缓存机制:每个 Computed 属性对应一个 Watcher,内部有一个属性
dirty。 - 初次访问:
dirty为true,执行求值函数,计算完成后将dirty置为false。 - 依赖收集:计算属性执行时,会访问响应式数据,从而让 Computed Watcher 订阅该数据的
Dep。 - 依赖触发:当数据改变时,通知 Computed Watcher,它并不立即重新计算,而是将
dirty设为true。此时,它会通知 Render Watcher 重新渲染。渲染时再次读取 Computed,发现dirty为true,从而获取新值。
2. Vue 3:基于 Effect 的响应式系统
Vue 3 彻底重构了响应式,不再使用 Watcher 类,而是引入了更轻量、更强大的 Effect(副作用函数) 概念。
A Vue 3 中的三种 Effect
Vue 3 的底层核心是 ReactiveEffect 类,所有的响应式行为都是一个 Effect。
| 类型 | 描述 |
|---|---|
| Component Effect | 负责组件渲染。Vue 3 的渲染函数包裹在 effect 中。 |
| Computed Effect | computed() 返回的是一个只读的响应式 ref。内部封装了一个 effect。 |
| Watch Effect | 开发者使用的 watch 或 watchEffect。 |
B Vue 3 底层源码剖析
(1)Computed 实现步骤
- 创建 Effect:调用
computed(getter)时,内部会创建一个ReactiveEffect实例。 - 调度器(Scheduler):这是关键。Computed 的 Effect 拥有一个定制的调度器。当依赖变动时,不执行 getter,而是将
_dirty设为true,并触发triggerRefValue(this)。 - 取值逻辑:
- 访问
.value时,触发trackRefValue(收集依赖)。 - 如果
_dirty为true,执行effect.run()重新求值并关闭_dirty。
- 访问
(2)Watch 实现步骤
- 标准化 Source:将输入的监视对象(ref, reactive, function)转化为一个 getter 函数。
- 定义 Job:创建一个调度任务
job,内部执行用户的callback。 - 创建 Effect:实例化
ReactiveEffect,并将job作为调度器。 - 执行:
- 如果是
immediate: true,立即执行。 - 正常情况下,当依赖数据变化,调度器
job被触发,执行回调。
- 如果是
3. Vue 2 vs Vue 3 深度对比
A 响应式劫持手段
- Vue 2: 使用
Object.defineProperty。缺点是无法监听对象属性的添加/删除,以及数组索引的直接修改(需要特殊 Hack)。 - Vue 3: 使用 ES6 Proxy。可以完美拦截对象的所有操作(
delete、has等)以及数组的所有变化,且性能更优(按需递归)。
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-if 或 v-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 2 | Vue 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 倍。
- 静态提升 (Static Hoisting):
Vue2 中无论节点是否变化,每次渲染都会重新创建 VNode。Vue3 会将那些永远不会变的静态节点提升到渲染函数之外,只在初始化时创建一次。 - 补丁标记 (Patch Flag):
Vue3 给包含动态内容的 VNode 加上了特定的标记(如:只有class变,或只有text变)。在 Diff 算法阶段,Vue3 只对比有标记的节点,不再全量对比。 - 缓存事件处理函数 (Cache Handlers):
默认情况下,Vue 会认为绑定事件的函数是动态的(每次渲染都可能变)。Vue3 会将其缓存起来,避免不必要的组件子树更新。
4. 源码体积与构建 (Tree-shaking)
- 按需编译: Vue2 的很多全局 API 是直接挂载在原型上的,即使不用,打包时也会包含在内。
- Tree-shaking 支持: Vue3 进行了重构,将 API 模块化。如果你没用到
watch或computed,打包工具(如 Vite/Webpack)就会自动剔除这些代码。这意味着最终生成的 Bundle 体积更小。
5. 其他新特性与内置组件
Vue3 引入了几个解决开发痛点的内置组件:
- Fragment(碎片): 支持多个根节点,减少了不必要的 DOM 层级(不用再为了满足 Vue2 规则强行套一层
div)。 - Teleport(传送门): 可以将子组件(如 Modal 弹窗)挂载到
body或其他指定的 DOM 节点上,解决了 CSS 样式嵌套导致的层级/遮挡问题。 - Suspense: 处理异步组件的加载状态,提供类似 Loading 的优雅过渡方案。
💡 总结:Vue3 面试回答口诀 (SOP)
- 底层看 Proxy: 解决了 Vue2 响应式的各种“坑”,监听更全面,性能更好。
- 模式看 Composition: 解决了逻辑复用难、代码维护难的问题,TS 支持更完美。
- 编译看静态提升: 通过
Patch Flag等手段让 Diff 算法变“聪明”了,非全量对比。 - 工程看 Tree-shaking: 模块化设计让体积更轻量。
更多推荐


所有评论(0)