2、详细说说vue2何vue3的工作原理以及区别?为什么vue3会做此升级?
目录
2. 编译优化 (Compiler Optimization) —— 核心升级
3. TypeScript 支持 (Developer Experience)
这是一个非常有分量的架构级问题。要讲清楚 Vue 2 到 Vue 3 的演进,我们需要从底层的响应式原理、虚拟 DOM (Virtual DOM) 的编译策略以及运行时性能三个维度深入剖析。
以下是从源码和架构设计层面的详细解读:
一、 什么是虚拟 DOM (Virtual DOM)?
在理解 Vue 原理之前,必须先理解虚拟 DOM。
1. 定义:
虚拟 DOM 本质上是一个 JavaScript 对象(VNode) ,它是真实 DOM 的“蓝图”或“替身”。
2. 为什么需要它?
- 操作真实 DOM 代价高昂:真实 DOM 节点非常重(包含大量属性和事件)。频繁操作 DOM 会导致浏览器频繁重排(Reflow)和重绘(Repaint),性能极差。
- JS 计算很快:在 JS 层面通过对比两个对象(新旧 VNode)的差异(Diff),计算出最小的更变操作,然后再去更新真实 DOM,效率最高。
3. 结构示例:
// 真实 DOM: <div class="box">Hello</div>
// 虚拟 DOM (VNode):
const vnode = {
tag: 'div',
props: { class: 'box' },
children: 'Hello',
// Vue3 新增了 patchFlag 等编译优化标记
}
二、 Vue 2 的工作原理
Vue 2 的核心是 Options API 和基于 Object.defineProperty 的响应式系统。
1. 响应式原理 (Reactivity)
Vue 2 在初始化(initState)时,会递归遍历 data 中的所有属性。
- 核心 API:
Object.defineProperty - 源码逻辑:
- Observer(观察者) :递归把对象属性转为 getter/setter。
- Dep(依赖容器) :每个属性闭包里都有一个
Dep实例,用来存放到到底谁用了我。 - Watcher(订阅者) :组件渲染函数、computed、watch 都是 Watcher。
-
工作流程:
-
(1)初始化:递归遍历 data 对象,用
Object.defineProperty劫持所有属性 - (2)依赖收集:渲染时触发 getter,收集 Watcher(观察者)
- (3)派发更新:数据变化触发 setter,通知所有 Watcher 更新
-
// Vue 2 响应式简化版
Object.defineProperty(obj, key, {
get() {
// 1. 依赖收集:如果当前有正在计算的 Watcher,就把它收集进 Dep
if (Dep.target) dep.depend();
return value;
},
set(newVal) {
if (newVal === value) return;
value = newVal;
// 2. 派发更新:通知 Dep 里所有的 Watcher 去 update
dep.notify();
}
});
2. Vue 2 的痛点
- 初始化慢:因为是递归遍历,如果
data对象很大,启动(Init)阶段会非常耗时,且内存占用高。 - 动态性不足:无法监听对象属性的新增(add)和删除(delete),必须用
$set/$delete。 - 数组限制:无法拦截数组索引修改(
arr[0] = 1),Vue 2 重写了数组的 7 个变异方法(push, pop...)来实现响应式。
3. 虚拟 DOM 与 Diff (Vue 2)
Vue 2 的 Diff 算法是 全量对比。
当数据变化时,Vue 2 会重新生成整个组件的 VNode 树,然后和旧的 VNode 树进行对比(双端比较算法)。即使有些节点及其子节点永远不会变(静态节点),Vue 2 依然会去比对它们。
三、 Vue 3 的工作原理
Vue 3 在响应式系统和编译优化上做了彻底的重构。
1. 响应式原理 (Reactivity)
Vue 3 使用 Proxy 替代了 defineProperty。代码位于 packages/reactivity。
- 核心 API:
Proxy+Reflect - 源码逻辑:
- 不再需要
Observer类,直接返回一个 Proxy 代理。 - Track(依赖收集) :当读取属性时触发
track(target, key),将副作用函数(Effect)存入全局的WeakMap。 - Trigger(派发更新) :当修改属性时触发
trigger(target, key),从WeakMap取出 Effect 执行。
- 不再需要
/ Vue 3 响应式实现
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// 依赖收集
track(target, key)
const result = Reflect.get(target, key, receiver)
// 懒递归:只有访问到深层属性时才代理
if (isObject(result)) {
return reactive(result)
}
return result
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
// 派发更新
if (oldValue !== value) {
trigger(target, key)
}
return result
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
trigger(target, key)
return result
}
})
}
- 优势:
- 懒代理(Lazy) :只有访问到深层对象时,才会将其转为 Proxy,初始化飞快。
- 全能拦截:支持新增、删除属性,支持数组索引修改,支持 Map/Set。
2. 编译优化 (Compiler Optimization) —— 核心升级
Vue 3 的 Diff 算法不仅仅是快,而是**“更聪明”** 。它在编译阶段(Template -> Render Function) 做了大量标记,让运行时(Runtime) 跑得更快。
- (1)PatchFlags (动态标记) :
在编译时,Vue 3 会分析模板,给动态节点打上“二进制标记”。
比如:<div :class="cls">123</div>。
Vue 3 知道只有class是动态的,Diff 时只对比class,完全忽略内容。
// 模板
<div>
<p>静态</p>
<p :class="dynamicClass">{{ text }}</p>
</div>
// Vue 3 编译后
render() {
return h('div', [
h('p', '静态'), // 无标记,跳过 Diff
h('p', { class: _ctx.dynamicClass }, _ctx.text,
PatchFlags.CLASS | PatchFlags.TEXT // 标记:只需对比 class 和 text
)
])
}
// PatchFlags 枚举
enum PatchFlags {
TEXT = 1, // 动态文本
CLASS = 2, // 动态 class
STYLE = 4, // 动态 style
PROPS = 8, // 动态属性
FULL_PROPS = 16, // 有动态 key 的属性
HYDRATE_EVENTS = 32 // 事件监听器
}
- (2)Block Tree (区块树) :
Vue 3 将模板切分成 Block,配合 PatchFlags,Diff 时直接跳过静态节点,只遍历动态节点数组。- Vue 2 Diff 复杂度 = 模板总体积
- Vue 3 Diff 复杂度 = 动态节点的数量
// 模板
<div>
<p v-if="ok">{{ msg }}</p>
<p v-else>{{ other }}</p>
</div>
// Vue 3 会创建 Block,收集所有动态节点
function render() {
return (openBlock(), createBlock('div', null, [
ok
? (openBlock(), createBlock('p', { key: 0 }, msg, PatchFlags.TEXT))
: (openBlock(), createBlock('p', { key: 1 }, other, PatchFlags.TEXT))
]))
}
// Block 会维护一个 dynamicChildren 数组
// Diff 时只对比这个数组,跳过静态节点
- (3)Hoist Static (静态提升) :
静态的节点(如<p>永远不变</p>)在内存中只创建一次,后续更新直接复用,不再重复创建 VNode。
// 编译前
<div>
<p>永远不变</p>
<p>{{ dynamic }}</p>
</div>
// Vue 2 编译后:每次都创建
render() {
return h('div', [
h('p', '静态文本'),
h('p', this.dynamic)
])
}
// Vue 3 编译后:静态节点提升到外部
const _hoisted_1 = h('p', '静态文本')
render() {
return h('div', [
_hoisted_1, // 复用
h('p', this.dynamic)
])
}
- (4)事件缓存:
// 模板
<button @click="handleClick">点击</button>
// Vue 2:每次都创建新函数
render() {
return h('button', {
onClick: this.handleClick // 每次渲染都是新引用
})
}
// Vue 3:缓存事件处理器
render() {
return h('button', {
onClick: _cache[0] || (_cache[0] = (...args) => _ctx.handleClick(...args))
})
}
四、 Vue 2 和 Vue 3 的比较与区别
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式底层 | Object.defineProperty |
Proxy |
| 检测能力 | 无法检测属性增删、数组索引修改 | 完全支持 |
| 初始化性能 | 递归遍历所有属性(慢、内存高) | 懒代理,访问时才转换(快) |
| 代码组织 | Options API (data, methods 分离) | Composition API (逻辑关注点聚合) |
| Diff 算法 | 全量双端比较,静态节点也要比 | 静态标记 + Block Tree,只比动态节点 |
| TypeScript | 支持较弱,类型推断困难 | 核心由 TS 编写,TS 支持极其友好 |
| 体积 | 较大,难以 Tree-shaking,~32KB | 模块化拆分,支持 Tree-shaking,体积更小,~10KB |
| 编译优化 | 无 | 静态提升、预字符串化、事件缓存 |
| Fragment | 组件只能有一个根节点 | 支持多根节点 (Fragment) |
五、 为什么 Vue 3 要做这些升级?
尤雨溪和团队进行 Vue 3 重构主要为了解决 Vue 2 的三个核心瓶颈:
1. 性能瓶颈 (Performance)
Vue 2 的响应式初始化是递归的,对于大数据量的表格或列表,启动非常慢。且 Diff 算法在大型复杂组件中,无谓的静态节点对比消耗了大量 CPU。
Vue 3 通过 Proxy 和编译优化(静态标记),实现了“按需响应”和“靶向更新”,性能大幅提升。
2. 代码组织与复用瓶颈 (Scalability)
在 Vue 2 的 Options API 中,一个功能的逻辑被拆分到 data, methods, watch 里。当组件变得巨大(几千行代码)时,维护代码需要在文件里上下反复横跳(Jumping)。且 Mixin 代码复用存在命名冲突和来源不清晰的问题。
Vue 3 引入 Composition API (组合式 API) ,允许开发者按“逻辑功能”组织代码,完美解决了大型项目的维护难题,Hooks 更是取代了 Mixin。
3. TypeScript 支持 (Developer Experience)
Vue 2 的源码是 JS 写的,通过 Flow 做类型检查,对 TS 的支持是后期补丁(this 指向在 TS 中很难推断)。随着前端工程化对 TS 需求的爆发,Vue 2 显得力不从心。
Vue 3 使用 TypeScript 重写,提供了原生的、极佳的类型推断体验。
六、面试怎么回答才精彩
回答框架(STAR 法则)
1. 开场(30 秒)
"Vue 2 和 Vue 3 的核心区别体现在响应式系统和编译优化两个方面。Vue 3 通过 Proxy 替代 Object.defineProperty,并引入了编译时优化策略,性能提升了 1.3-2 倍。"
2. 响应式对比(1 分钟)
// 举例说明
"Vue 2 使用 Object.defineProperty,存在三个问题:
1. 无法检测对象新增属性:obj.newKey = 'value' 不是响应式
2. 无法检测数组索引变化:arr[0] = 'new' 不是响应式
3. 初始化时需要递归遍历整个对象,性能差
Vue 3 使用 Proxy 完美解决:
1. 可以拦截所有操作,包括新增、删除
2. 支持数组、Map、Set 等数据结构
3. 懒代理,只有访问深层属性时才递归,性能更好"
3. 编译优化(重点,2 分钟)
"Vue 3 最大的升级是编译时优化,主要有四个策略:
① 静态提升:静态节点提升到渲染函数外部,避免重复创建
② Patch Flags:给动态节点打标记,Diff 时只对比标记的属性
// 例如这个节点只有 text 是动态的
<p>{{ msg }}</p>
// 编译后会标记 PatchFlags.TEXT
// Diff 时只对比 textContent,跳过其他属性
③ Block Tree:收集所有动态节点到数组,Diff 时直接遍历数组,跳过静态节点
④ 事件缓存:缓存事件处理器,避免每次渲染创建新函数"
4. 实际收益(30 秒)
"这些优化带来的实际收益是:
- 首次渲染快 55%,更新快 133%
- 内存占用减少 54%
- 打包体积从 32KB 降到 10KB(Tree-shaking)"
5. 升级原因(30 秒)
"Vue 3 升级的根本原因是:
- 解决 Vue 2 的技术债务(Object.defineProperty 局限)
- 性能优化需求(大型应用场景)
- 开发体验提升(Composition API、TS 支持)
- 拥抱现代浏览器特性,为未来留出优化空间"
加分项:深入细节
如果面试官追问,可以补充:
Diff 算法对比
// Vue 2:双端比较,全量 Diff
function updateChildren(oldCh, newCh) {
// 头头、尾尾、头尾、尾头四种情况对比
// 所有节点都要遍历
}
// Vue 3:快速路径 + 最长递增子序列
function patchKeyedChildren(oldCh, newCh) {
// 1. 前置节点对比(相同的跳过)
// 2. 后置节点对比(相同的跳过)
// 3. 只对中间变化的部分用最长递增子序列算法
// 4. 配合 Block Tree,只对比动态节点
}
响应式性能对比
// Vue 2:初始化 10000 个属性需要递归 10000 次
const data = { /* 10000 个属性 */ }
// 每个属性都要 Object.defineProperty
// Vue 3:只代理第一层,访问时才递归
const data = reactive({ /* 10000 个属性 */ })
// 只创建一个 Proxy,访问深层属性时才递归
总结话术
"总的来说,Vue 3 是一次编译时 + 运行时的全面升级。通过 Proxy 解决了响应式的历史问题,通过编译优化实现了靶向更新,最终达到了性能翻倍、体积减半的效果。这也体现了现代前端框架的发展趋势:把更多工作交给编译器,让运行时更轻量。"
面试技巧:
- 先说结论,再展开细节
- 用代码示例说明问题
- 强调实际收益(数据说话)
- 展示对框架设计思想的理解
- 保持逻辑清晰,分点阐述
总结
- 原理层面:Vue 2 是劫持 setter/getter,Vue 3 是代理整个对象。
- 更新机制:Vue 2 是全量树对比,Vue 3 是基于静态标记的动态节点追踪。
- 目的:Vue 3 的升级是为了更快(性能)、更小(体积)、更易维护(组合式 API)以及更好的 TS 支持。
更多推荐


所有评论(0)