Vue2与Vue3工作原理对比
首先说共同点:Vue 2 和 Vue 3 的核心架构都是基于响应式系统和虚拟 DOM 的。这是 Vue.js 框架设计的基石;区别是Vue 3 在实现这两个核心系统的方式上进行了大量的重构和优化,这使得它在性能、可维护性和开发体验上都有了显著的提升。
-
Vue 2:基于 Object.defineProperty 的响应式系统,选项式 API,代码组织相对松散。
-
Vue 3:基于 Proxy 的响应式系统,组合式 API,采用模块化架构,性能更高,灵活性更强。
-
Vue 2 和 Vue 3 都坚守了“响应式数据 + 虚拟 DOM Diff "
1. 响应式系统工作原理
1.1 vue2
1.1.1 vue2 的响应式系统工作
Vue 2 通过Object.defineProperty实现响应式系统。vue2 通过递归地遍历数据对象的所有属性,并使用 Object.defineProperty 将它们转换为 getter 和 setter。
- 依赖收集(Dependency Collection):当组件首次渲染时,会“读取‘‘数据属性,触发 Getter。Vue 会把这个“读取操作”(即这个组件对应的 Watcher 实例)收集为这个属性的“依赖”。
- 派发更新(Dependency Notification):当数据被修改时,会触发 Setter。Setter 会通知之前收集的所有依赖(Watcher),数据被修改。
- Watcher:每个组件实例都有一个对应的 Watcher。当它收到属性的变化通知后,会触发组件的重新渲染。
局限性:
- 无法检测对象属性的添加或删除(需要使用 Vue.set 或 this.$set)。
- 对数组的某些操作(如通过索引直接设置项 arr[index] = newValue,或修改数组长度 arr.length = newLength)无法被检测,Vue 重写了数组的变异方法(如 push, pop, splice 等)来弥补这一点。
1.1.2 vue2 的响应式实现
数据对象转为响应式的方法:
const data = {
user: {
name: '张三',
address: {
city: '北京',
street: '长安街'
}
},
scores: [85, 90, 78]
}
// obj - 目标对象, key - 属性名,val - 属性值
//使用示例 defineReactive(data , 'scores', data .scores)
function defineReactive(obj, key, val) {
// 关键代码:如果值是对象,递归处理
if (typeof val === 'object' && val !== null) {
observe(val) // 递归调用
}
Object.defineProperty(obj, key, {
get() {
return val
// return obj[key] // 这里又会触发 getter,无限递归!
},
set(newVal) {
if (newVal === val) return
val = newVal
//obj[key] = newVal // 这里又会触发 setter,无限递归!
// 新值也可能是对象,需要递归处理
if (typeof newVal === 'object' && newVal !== null) {
observe(newVal)
}
}
})
}
递归方法observe(val)的实现
// 观察者构造函数
class Observer {
constructor(value) {
this.value = value
this.dep = new Dep()
// 标记该对象已被观察
def(value, '__ob__', this)
if (Array.isArray(value)) {
// 数组的特殊处理
this.observeArray(value)
} else {
// 对象的处理
this.walk(value)
}
}
// 遍历对象的所有属性
walk(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
//defineReactive调用了observe,observe有在此使用了defineReactive
defineReactive(obj, keys[i], obj[keys[i]])
}
}
// 观察数组的每个元素
observeArray(items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
// 主要的 observe 函数
function observe(value) {
// 如果不是对象或者是 VNode 实例,直接返回
if (typeof value !== 'object' || value === null) {
return
}
let ob
// 如果已经有 Observer 实例,直接返回
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else {
// 创建新的 Observer 实例
ob = new Observer(value)
}
return ob
}
遍历过程:
- 创建 Observer 实例,调用 walk(data)
- 遍历 data 的属性,调用 defineReactive(data, ‘level1’, data.level1)
- 发现 data.level1 是对象,调用 observe(data.level1)
- 创建 Observer 实例,调用 walk(data.level1)
- 遍历 data.level1 的属性,调用 defineReactive(data.level1, ‘level2’, data.level1.level2)
- 发现 data.level1.level2 是对象,调用 observe(data.level1.level2)
- 创建 Observer 实例,调用 walk(data.level1.level2)
- 遍历 data.level1.level2 的属性,调用 defineReactive(data.level1.level2, ‘level3’, ‘最终值’)
用递归确保对象的深度响应式,但是数组使用重写数组方法的方式实现响应式。
可以说Vue 2 中所有对象的属性和基本类型数据在初始化时都会用到 defineReactive,但数组有特殊的处理机制,后期动态添加的对象属性(需要 Vue.set)和methods中的函数都不使用 defineReactive 。
1.1.3 声明对象数据后调用 defineReactive的过程
//声明数据,此时只是普通的 JavaScript 对象声明 还没有任何 Vue 的响应式处理
const data = {
user: { name: '张三' },
list: [1, 2, 3],
count: 0
}
//当这个对象被 Vue 实例使用时,才会开始响应式处理:
new Vue({
data() {
return {
user: { name: '张三' },
list: [1, 2, 3],
count: 0
}
}
})
vue文件结构:
<template>
<div>
<p>{{ message }}</p>
<p>{{ user.name }}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello Vue',
user: { name: '张三' },
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}
</script>
.vue文件中声明的对象数据会经过一个编译和构建过程才被 Vue 实例使用
阶段 1: Vue Loader 处理 .vue 文件
vue-loader 的工作流程:
- 解析 .vue 文件,分离 template、script、style
- 将 template 编译为 render 函数
- 将 script 部分转换为 JavaScript 模块
阶段 2: 编译后的结果
// 编译后的 JavaScript 文件大致如下:
export default {
data() {
return {
message: 'Hello Vue',
user: { name: '张三' },
count: 0
}
},
methods: {
increment() {
this.count++
}
},
// template 被编译为 render 函数
render(h) {
return h('div', [
h('p', this.message),
h('p', this.user.name)
])
}
}
main.js文件执行new Vue({})进行数据的初始化时会调用defineReactivedefineReactive将数据改为响应式。
1.1.4 总结
在 main.js 中执行 new Vue() 时:
全局组件:已经在注册时处理完成,new Vue() 时直接可用
局部组件:在渲染过程中按需解析和初始化
根组件数据:new Vue() 时立即初始化根组件的响应式数据
子组件数据:在组件被渲染创建实例时才初始化
所以 new Vue() 会触发组件初始化链,从根组件开始,逐步初始化所有用到的组件及其数据。
1.2 Vue3
1.2.1 Vue3 的响应式系统工作
Vue 3 使用 ES6 的 Proxy 来重写了响应式系统, Proxy 的主要作用是拦截并监听对象的各种操作,从而实现数据的响应式更新。它解决了 Vue 2 的局限性。
-
Proxy:可以创建一个对象的代理,从而拦截并定义该对象的基本操作(如属性读取、赋值、删除、遍历等)。
-
工作原理:
拦截:当你访问或修改一个响应式对象的任何属性时,Proxy 都能拦截到。 依赖收集:拦截 get 操作,收集依赖。 派发更新:拦截 set 或 delete 操作,通知所有依赖。
优势:
- 完美支持对象和数组:可以检测到属性的添加、删除,以及通过索引修改数组等操作。
- 性能更好:不需要像 Vue 2 那样递归遍历所有属性并一次性转换,而是“按需”转换(只有在访问某个嵌套属性时才会将其转换为响应式)。
1.2.2 Proxy 在 Vue 3 中的具体作用
import { reactive } from 'vue';
const target = {
name: '小明',
age: 18,
hobbies: ['篮球', '编程']
};
const state = reactive(target); // 这里返回的就是一个 Proxy 代理对象
当你操作 state 时,Proxy 会进行拦截:
拦截 get 操作(读取属性)
当你 console.log(state.name),Proxy 会触发 get 陷阱。
Vue 会在这里进行依赖收集(Track),记录下“这个组件或计算属性依赖了state.name”。
拦截 set 操作(设置属性)
当你 state.age = 19,Proxy 会触发 set 陷阱。
Vue 会在这里触发更新(Trigger),通知所有依赖 state.age 的地方(例如组件模板)进行更新。
拦截其他操作
delete state.property: 拦截 delete 操作,使属性删除也是响应式的。
state.hobbies.push(‘音乐’): 首先会 get hobbies 数组,然后执行 push。因为 hobbies 本身也是一个 Proxy 代理数组,所以 push 操作也会被拦截,并触发视图更新。
‘name’ in state: 拦截 in 操作符。
Object.keys(state): 拦截 Object.keys() 等迭代操作。
reactive() 返回的是一个原始对象的 Proxy,它和原始对象是不相等的:console.log( reactive(target ) === target ) // false
只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用 Vue 的响应式系统的最佳实践是仅使用你声明对象的代理版本。
对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身:
// 在同一个对象上调用 reactive() 会返回相同的代理
const state = reactive(target);
console.log(reactive(target ) === state ) // true
// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(state ) === state ) // true
1.2.3 基础 Proxy 响应化实现
// 原始数据
const data = {
user: { name: '张三' },
list: [1, 2, 3],
count: 0
};
// 存储依赖关系
const targetMap = new WeakMap();
let activeEffect = null;
// 创建响应式代理
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
console.log(`读取属性: ${key}`, target[key]);
// 依赖收集
track(target, key);
const result = Reflect.get(target, key, receiver);
// 如果是对象,递归代理(惰性代理)
if (result !== null && typeof result === 'object') {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
console.log(`设置属性: ${key}`, value);
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
// 值发生变化时触发更新
if (!Object.is(oldValue, value)) {
trigger(target, key, value, oldValue);
}
return result;
},
deleteProperty(target, key) {
console.log(`删除属性: ${key}`);
const hadKey = Object.prototype.hasOwnProperty.call(target, key);
const result = Reflect.deleteProperty(target, key);
if (hadKey && result) {
trigger(target, key, undefined, target[key]);
}
return result;
}
});
}
// 依赖收集
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
dep.add(activeEffect);
}
// 触发更新
function trigger(target, key, newValue, oldValue) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
if (effects) {
effects.forEach(effect => {
effect();
});
}
}
// 创建副作用函数
function effect(fn) {
activeEffect = fn;
fn(); // 首次执行,建立依赖关系
activeEffect = null;
}
1.2.4 reactive使用proxy的局限
- 有限的值类型:它只能用于对象类型 (对象、数组和如 Map、Set 这样的集合类型)。它不能持有如 string、number 或 boolean 这样的原始类型。
- 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失:
let state = reactive({ count: 0 })
// 上面的 ({ count: 0 }) 引用将不再被追踪
// (响应性连接已丢失!)
state = reactive({ count: 1 })
- 对解构操作不友好:当将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,将丢失响应性连接:
const state = reactive({ count: 0 })
// 当解构时,count 已经与 state.count 断开连接
let { count } = state
// 不会影响原始的 state
count++
// 该函数接收到的是一个普通的数字
// 并且无法追踪 state.count 的变化
// 我们必须传入整个对象以保持响应性
callSomeFunction(state.count)
由于这些限制,建议使用 ref() 作为声明响应式状态的主要 API。
1.2.5 ref 在模板中解包
在模板渲染上下文中,只有顶级的 ref 属性才会被解包。
const count = ref(0)
const object = { id: ref(1) }
<div> {{count + 1 }}</div> 正常工作
<div> {{object.id + 1 }}</div> 不会正常工作
{{object.id + 1 }}不正常工作原因是在计算表达式时 object.id 没有被解包仍然是一个 ref 对象。
可以先进行结构将 id 解构为一个顶级属性:const { id } = object;
{{ id + 1 }}正常工作
如果 ref 是文本插值的最终计算值 (即 {{ }} 标签),那么它将被解包,因此以下内容将渲染为 1
{{ object.id }}
2. 虚拟 DOM(Virtual DOM)与渲染机制
即使响应式系统通知了“数据变了”,直接操作真实 DOM 也是非常消耗性能的(因为 DOM 操作会触发浏览器的重绘和重排)。
什么是虚拟 DOM?
它是一个用 JavaScript 对象(VNode)来描述真实 DOM 结构和属性的轻量级副本。
2.1虚拟 DOM工作流程:渲染和更新
Vue 2 还是 Vue 3,其虚拟 DOM 的核心工作流程都可以概括为以下三个步骤:
-
编译阶段:将模板编译成渲染函数。
-
运行时-渲染阶段:执行渲染函数,生成虚拟 DOM 树。
-
运行时-挂载与更新阶段:将虚拟 DOM 树转换为真实 DOM,并在数据变化时进行高效的比对和更新。
2.2 Vue 2 的虚拟 DOM 工作流程
Vue 2 的流程相对直接,但存在一些全量比对的开销。
流程详解:
- 编译:Vue 2 的编译器将模板解析成 AST(抽象语法树),然后优化静态节点,最后生成渲染函数。这个渲染函数本质上是一个 createElement(或 h)函数的调用集合。
Vue 的模板()会被编译成一个 渲染函数(Render Function)。这个渲染函数被执行时,会返回一个虚拟 DOM 树(VNode Tree)
- 渲染:当响应式数据发生变化或组件初次渲染时,会执行渲染函数。这个函数会递归地创建并返回一个完整的 虚拟 DOM 树(VNode Tree)。每个 VNode 都是一个普通的 JavaScript 对象,描述了对应节点的信息(标签名、属性、子节点等)。
将生成的虚拟 DOM 树转换成真实的 DOM 节点,并插入到页面中。
-
挂载与更新(Patch):
-
初次渲染:将生成的 VNode 树递归地创建成真实的 DOM 节点,并挂载到页面上。
-
更新:当数据变化导致重新渲染时,会再次执行渲染函数,生成一个新的 VNode (虚拟 DOM)树。然后,Vue 会将 新旧两棵 VNode 树 进行比对,这个过程叫做 Diff。
-
Diff 算法:Vue 2 采用了一种高效的 同层比较 算法。它只会对同一层级的节点进行比较,而不会跨层级。这大大减少了计算量。比较过程中,它会尽可能地复用相同类型的节点,只更新发生变化的属性或文本内容。这个算法的目标是找出最小差异。
-
Patch:根据 Diff 的结果将计算出的差异(需要进行的 DOM 操作),将需要更新的变更批量应用到真实的 DOM 上。
-
Vue 2 的特点与瓶颈:
-
全量 Diff:只要数据变化,就会从变化的组件开始,生成完整的 VNode 树,并对整棵树进行 Diff。即使其中只有一部分节点是动态的。
-
静态节点优化有限:虽然编译器会标记静态节点并在后续渲染中复用它们,但在 Diff 过程中,仍然需要去遍历这些静态节点的父级。
2.3 Vue 3 的虚拟 DOM 工作流程
Vue 3 在 Vue 2 的基础上,通过编译时的优化和运行时的改进,实现了更快的更新速度。
动态节点指的是在组件的模板中,其内容、属性或结构会随着组件响应式数据的变化而改变的 DOM 节点。
流程详解与优化点:
-
编译时优化:这是 Vue 3 性能提升的关键。编译器会进行非常积极的静态分析。
-
Patch Flags(补丁标志):编译器会分析模板中的动态绑定(如 :class、:id、{{ text }})。在为动态节点创建 VNode 时,会为其打上一个或多个 patchFlag。例如,1 代表文本动态,8 代表 props 动态。这样在运行时,Vue 就知道这个节点具体是哪部分需要更新,无需全量对比属性。
-
Block Tree(块树):Vue 3 将模板划分为“块”。一个“块”是一个内部结构,它追踪其所有 动态后代节点。根节点是一个“Block”。带有 v-if、v-for 等结构指令的节点也会创建一个新的“Block”。这样,在更新时,Vue 只需要遍历这些 Block 中的动态节点列表,而无需遍历整棵完整的树。
-
静态提升(Hoisting):在模板中完全静态的节点会被提升到渲染函数之外。这意味着它们只会在首次渲染时创建一次,在后续的每次渲染中都会被复用,避免了重复创建 VNode 的开销。
-
树结构拍平:得益于 Block Tree,一个 Block 内部的所有动态节点会被收集到一个扁平数组中。当更新发生时,Vue 直接遍历这个数组进行 Diff,完全跳过了静态节点和静态父节点。这大大减少了需要遍历的 VNode 数量。
-
-
运行时优化:
-
靶向更新:由于在编译阶段已经通过 Patch Flags 和 Block Tree 标记了动态节点及其类型,在运行时的 Diff 过程中,Vue 3 可以做到 “靶向更新”。它直接找到动态节点,并根据其 patchFlag 只更新对应的部分(如只更新文本内容或只更新某个 class)。
-
更高效的 Diff 算法:在遇到动态子节点数组(如 v-for)时,Vue 3 使用了 最长递增子序列 算法来优化对比过程,尽可能多地复用 DOM 元素,减少移动操作。
-
Vue 3如何利用“动态节点”进行优化
Vue 3 的编译器会像侦探一样,在编译阶段就找出所有这些动态节点,并做好标记:
- Patch Flags(补丁标志):给每个动态节点打上一个数字标签,指明其动态类型。
- 例如,1 代表文本动态,8 代表属性动态。
- 好处:运行时无需做“全量对比”。看到一个节点有 PATCH_FLAG_TEXT,就直接 更新其 textContent,看到 PATCH_FLAG_PROPS,就只对比更新其动态属性列表。
- Block Tree(块树) 与 树结构拍平:
- 编译器会将模板组织成一个个“块”(Block)。每个块都知道自己内部包含了哪些动态子节点。
- 在更新时,Vue 不再需要递归地遍历整棵 VNode 树。它只需要直接进入这个“块”,然后遍历其内部维护的一个扁平化的动态节点数组。
- 这就意味着,所有静态节点和它们的静态父节点都被完全跳过,根本不会进入 Diff 流程。
2.4 核心差异对比
| 特性 | vue2 | vue3 |
|---|---|---|
| Diff 策略 | 全量 Diff:从根组件开始,对整棵 VNode 树进行同层比较。 | 靶向 Diff:通过 Block Tree 只对比动态节点,跳过静态内容。 |
| 静态内容处理 | 静态节点会被复用,但其父级仍需被遍历。 | 静态提升 和 树结构拍平,完全跳过静态节点的创建和比对。 |
| 动态节点识别 | 无明确标志,需要对比所有属性来判断是否更新。 | Patch Flags 明确标记动态绑定的类型,更新时精准定位。 |
| 性能影响 | 与模板大小相关,即使大部分是静态内容,更新时也有固定开销。 | 更新开销主要与动态节点的数量相关,模板大小影响减小。 |
| 编译产物 | 渲染函数生成的 VNode 结构相对统一,信息量少。 | 渲染函数包含优化信息(如 openBlock, createBlock),VNode 携带 patchFlag、dynamicChildren 等。 |
Diff 算法避免了“推倒重来”,只更新必要的部分,减少了直接且频繁的 DOM 操作提高了效率。
3. 整体工作流程总结
-
初始化:
-
创建 Vue 实例。
-
初始化响应式系统:Vue 遍历 data, props 等选项,使用 Object.defineProperty(Vue 2)或 Proxy(Vue 3)将它们转换为响应式数据。
-
编译模板:将模板编译成渲染函数。
-
-
首次渲染:
-
执行渲染函数。在执行过程中,会读取响应式数据,触发它们的 Getter。
-
依赖收集:响应式系统将当前组件的 Watcher(或副作用函数)记录为这些数据的“依赖”。
-
渲染函数返回一个虚拟 DOM 树。
-
通过 patch 函数将虚拟 DOM 转换为真实 DOM,显示在页面上。
-
-
数据变化与更新:
-
用户交互或异步操作导致数据发生变化。
-
触发 Setter:修改数据时,触发响应式数据的 Setter。
-
通知更新:Setter 通知该数据的“依赖”(即组件的 Watcher):“数据已更新”。
-
异步更新队列:Vue 不会立即执行更新,而是将 Watcher 推进一个异步队列。在下一个事件循环“tick”中,Vue 会清空这个队列,执行所有 Watcher 的更新。这避免了同一个数据多次变化导致的重复渲染。
-
重新渲染与打补丁:
-
被触发的 Watcher 执行组件的渲染函数,生成一个新的虚拟 DOM 树。
-
将新旧虚拟 DOM 树进行 Diff 对比,找出差异。
-
将差异通过 patch 函数应用到真实 DOM上,完成视图更新。
-
4. 总结
Vue.js 的工作原理可以概括为:
通过响应式系统自动追踪数据依赖,在数据变化时通过虚拟 DOM 的 Diff 算法计算出最小化更新,并高效地 patch 到真实 DOM 上,从而实现数据到视图的自动、高性能同步。
这种机制让开发者可以从繁琐的 DOM 操作中解放出来,只需声明式地描述“视图应该是什么样子”,而将如何高效更新的复杂问题交给 Vue 框架本身。
更多推荐

所有评论(0)