从 Vue2 到 Vue3:响应式系统的演进与实现
Vue.js 作为当前最受欢迎的前端框架之一,其响应式系统是其核心特性。从 Vue2 到 Vue3,响应式系统经历了重大的架构升级和性能优化。本文将深入探讨这一演进过程,帮助开发者理解两个版本响应式系统的设计思想、实现原理以及它们之间的差异。
Vue2 响应式系统回顾
观察者模式的应用
在深入了解 Vue2 响应式系统之前,我们先来理解其背后的设计模式——观察者模式。
想象这样一个场景:你想要买一个三折叠,但是目前店里还没货。那你如何才能及时买到呢?
有两种不太理想的方案:
- 你每天去一趟商场检查是否到货
- 店里商品到货后向所有顾客发出通知
这两种方案都存在问题:要么让你浪费时间,要么让商店浪费资源。
解决方案:让有需求的顾客(观察者/watcher)主动订阅,之后商店(发布者/dep)只需要给订阅了的用户发送通知。
Vue2 响应式工作机制
Vue2 的响应式系统正是基于观察者模式实现的:
-
数据劫持:data 中的数据会被 Vue 遍历,通过
Object.defineProperty生成 getter 和 setter,这样当访问或设置属性时,Vue 就有机会做一些额外的事情。 -
依赖收集:每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把"接触"过的数据属性记录为依赖。
-
派发更新:当依赖项的 setter 触发时,会通知 watcher,从而使关联的组件重新渲染。

核心组件详解
1. Observer(数据劫持)
Observer 的工作是把一个普通的对象转换为响应式的对象。它通过 Object.defineProperty 把对象的每个属性转换为带有 getter 和 setter 的属性。

// 核心实现逻辑
function defineReactive(obj, key, val) {
// 递归处理嵌套对象,确保深层属性也具有响应性
observe(val)
// 每个属性对应一个依赖管理器
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 依赖收集:将当前活跃的 Watcher 添加到依赖列表
if (Dep.target) {
dep.depend()
}
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
// 新值如果是对象,需要递归处理为响应式
observe(newVal)
// 通知所有依赖更新
dep.notify()
}
})
}
// 递归处理对象的所有属性
function observe(obj) {
if (typeof obj !== 'object' || obj === null) {
return
}
new Observer(obj)
}
class Observer {
constructor(value) {
this.value = value
if (Array.isArray(value)) {
// 处理数组:改写数组原型方法
this.observeArray(value)
value.__proto__ = arrayMethods
} else {
// 处理对象:遍历所有属性并定义响应式
this.walk(value)
}
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
observeArray(arr) {
arr.forEach(item => observe(item))
}
}
Observer 是 Vue 内部的构造器,在组件生命周期中,这件事发生在 beforeCreate 之后,created 之前。实现上:
- 它会递归遍历对象的所有属性,完成深度的属性转换。
- 由于遍历时只能遍历到对象的当前属性,因此无法监测动态增加或删除的属性,需要使用
$set和$delete方法,让开发者通过这两个实例方法对已有响应式对象添加或删除属性。 - 对于数组,需要重写原型方法来监听变化。
总的来说,Observer 的目标,就是要让一个对象,它属性的读取、赋值,内部数组的变化都要能够被 Vue 感知到。
2. Dep(发布者/依赖管理器)
Dep 是依赖管理器,核心功能如下:
- 添加观察者:当读取响应式对象的某个属性时,进行依赖收集
- 通知观察者:当改变某个属性时,派发更新

class Dep {
static target = null // 当前活跃的 Watcher
constructor() {
this.subs = [] // 存储依赖的 Watcher
}
// 添加依赖
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知所有依赖更新
notify() {
const subs = this.subs.slice()
subs.forEach(sub => sub.update())
}
// 添加 Watcher 到依赖列表
addSub(sub) {
this.subs.push(sub)
}
// 移除 Watcher
removeSub(sub) {
remove(this.subs, sub)
}
}
3. Watcher(观察者)
Watcher 是观察者,当依赖的数据发生变化时,Dep 会通知 Watcher 调用 update 方法来更新数据。
class Watcher {
constructor(vm, expOrFn, cb, options) {
this.vm = vm
this.cb = cb
this.deps = [] // 存储该 Watcher 依赖的 Dep
this.depIds = new Set() // 用于去重
// 解析表达式,生成更新函数
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
// 初始化时触发依赖收集
this.value = this.get()
}
// 触发 getter 并收集依赖
get() {
Dep.target = this // 设置当前活跃的 Watcher
const value = this.getter.call(this.vm, this.vm) // 执行 getter,触发数据的 getter
Dep.target = null // 重置
return value
}
// 接收更新通知
update() {
// 放入调度器队列
queueWatcher(this)
}
// 实际执行更新
run() {
const newValue = this.get()
const oldValue = this.value
if (newValue !== oldValue) {
this.value = newValue
this.cb.call(this.vm, newValue, oldValue)
}
}
// 添加依赖
addDep(dep) {
const id = dep.id
if (!this.depIds.has(id)) {
this.depIds.add(id)
this.deps.push(dep)
dep.addSub(this)
}
}
}
4. Scheduler(调度器)
Dep 通知 watcher 之后,如果 watcher 执行重运行对应的函数,就有可能导致函数频繁运行,从而导致效率低下。为了避免这个问题,Vue2 引入了调度器。
调度器维护维护一个执行队列,同一个 watcher 仅存在一次,队列中的 watcher 不是立即执行,而是通过 nextTick 把更新操作放入微队列中异步执行,也就是说,当响应式数据变化时,render 函数的执行是异步的,并且在微队列中。
const queue = []
let has = {} // 用于去重
let waiting = false
function queueWatcher(watcher) {
const id = watcher.id
if (!has[id]) {
has[id] = true
queue.push(watcher)
// 延迟执行 flushSchedulerQueue
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
function flushSchedulerQueue() {
queue.sort((a, b) => a.id - b.id) // 按 id 排序,确保父组件先于子组件更新
// 执行所有 Watcher 的 run 方法
for (let i = 0; i < queue.length; i++) {
const watcher = queue[i]
watcher.run()
}
// 重置队列
resetSchedulerState()
}
// 利用 Promise/MutationObserver/setImmediate/setTimeout 实现异步更新
function nextTick(cb) {
callbacks.push(cb)
if (!pending) {
pending = true
if (typeof Promise !== 'undefined') {
Promise.resolve().then(flushCallbacks)
} else if (typeof MutationObserver !== 'undefined') {
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(0))
observer.observe(textNode, { characterData: true })
textNode.data = 1
} else if (typeof setImmediate !== 'undefined') {
setImmediate(flushCallbacks)
} else {
setTimeout(flushCallbacks, 0)
}
}
}
Vue2 响应式整体流程如下:

尽管 Vue2 的响应式系统设计精巧,但仍存在一些局限性:
- 拦截范围有限:
Object.defineProperty只能拦截对象特定属性的读写操作 - 无法检测新增/删除属性:需要使用
$set和$delete - 数组监听复杂:需要重写数组原型方法
- 性能开销:需要递归遍历所有属性
Vue3 响应式系统的革新
Vue3 对响应式系统进行了彻底重构,基于 Proxy 实现了更强大、更灵活的响应式能力,同时保持了更好的性能和开发体验,主要体现在三个方面:
1. 数据拦截方式的变化
Vue2:使用 Object.defineProperty 进行拦截
Vue3:使用 Proxy + Object.defineProperty 进行拦截
- ref:使用
Object.defineProperty + Proxy方式 - reactive:使用
Proxy方式
两者的共同点:
- 都可以针对对象成员拦截
- 都可以实现深度拦截
两者的差异点:
- 拦截广度:
Object.defineProperty针对对象特定属性的读写操作进行拦截,新增/删除的属性无法侦测Proxy针对整个对象的多种操作进行拦截,包括属性读取、赋值、删除、描述符获取等
- 性能:在大多数场景下,Proxy 比 Object.defineProperty 效率更高,拦截方式更加灵活
2. 创建响应式数据的变化
Vue2:通过 data 选项创建响应式数据
Vue3:通过 ref、reactive 等方法创建响应式数据
ref 的实现原理
class RefImpl<T> {
private _value: T
private _rawValue: T
public dep?: Dep = undefined
public readonly __v_isRef = true
constructor(
value: T,
public readonly __v_isShallow: boolean,
) {
this._rawValue = __v_isShallow ? value : toRaw(value)
// 有可能是原始值,有可能是 reactive 返回的 proxy
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
// 收集依赖
return this._value
}
set value(newVal) {
// 派发更新
}
}
// 判断是否是对象,是对象就用 reactive 来处理,否则返回原始值
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
reactive 的实现原理
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>,
) {
// 创建 Proxy 代理对象
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
)
proxyMap.set(target, proxy)
return proxy
}
export function reactive(target: object) {
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap,
)
}
3. 依赖收集机制的变化
Vue2:Watcher + Dep
- 每个响应式属性都有一个 Dep 实例,用于依赖收集
- Dep 内部包含一个数组,存储依赖这个属性的所有 watcher
- 当属性值发生变化,dep 通知所有 watcher 进行更新
Vue3:WeakMap + Map + Set
- Vue3 的依赖收集粒度更细
- WeakMap 的键对应响应式对象,值是一个 Map
- Map 的键是该对象的属性,值是一个 Set
- Set 里面存储了所有依赖于这个属性的 effect 函数
// Vue3 依赖收集结构
const targetMap = new WeakMap()
// targetMap: WeakMap {
// target1: Map {
// 'property1': Set { effect1, effect2 },
// 'property2': Set { effect3 }
// }
// }
Vue3 响应式系统的优势
- 更强的拦截能力:Proxy 可以拦截更多操作类型
- 更好的性能:按需代理,避免了递归遍历所有属性
- 更细粒度的依赖追踪:从组件级别细化到 effect 函数级别
- 更好的 TypeScript 支持:类型推导更加准确
- 更灵活的 API:ref 和 reactive 提供了更多选择
总结
Vue3 的响应式系统相比 Vue2 有了质的飞跃:
- 数据拦截方面:Vue2 受限于
Object.defineProperty的能力,不得不通过各种补丁(如$set、数组方法重写)适配开发需求;Vue3 则借助 Proxy 实现了更自然的响应式体验。 - 语法层面":Vue2 的响应式系统隐藏在
data选项背后,开发者难以直接操控;Vue3 提供的reactive、ref等 API 让响应式机制更透明,也更易于扩展。 - 依赖收集方面:Vue2 的 Watcher 与组件强绑定;Vue3 的副作用函数模型使响应式数据可以脱离组件单独使用,更适合跨组件状态管理。
更多推荐



所有评论(0)