2864 words
14 minutes
Vue2 和 Vue3 实现响应式的原理

Vue 为什么“改了数据,页面就自动更新”?这件事的本质,就是响应式系统。而 Vue2 和 Vue3 最大的底层变化之一,正是响应式实现方式的升级:Vue2 主要依赖 Object.defineProperty,Vue3 则以 Proxy 为核心。

如果只用一句话概括两代实现差异:

  • Vue2:劫持对象已有属性的 getter / setter,通过 Dep + Watcher 做依赖收集和派发更新
  • Vue3:通过 Proxy 代理整个对象,通过 track + trigger + effect 构建更通用、更完整的响应式系统

一、先理解:什么叫“响应式”?#

所谓响应式,本质上就是:

当数据变化时,依赖这个数据的逻辑能够自动重新执行。

例如:

const state = { count: 0 }
function render() {
console.log(`count is ${state.count}`)
}

普通 JavaScript 里,如果你改了:

state.count++

render() 不会自动再跑一次。

而 Vue 的目标就是:

  1. 记录“谁依赖了这个数据”
  2. 当数据变化时,自动通知这些依赖重新执行

这里面有三个关键问题:

  1. 怎么知道数据被读取了?
  2. 怎么知道数据被修改了?
  3. 怎么找到依赖它的副作用并重新执行?

Vue2 和 Vue3 的实现差异,主要就体现在这三点上。


二、Vue2 的响应式原理:Object.defineProperty#

Vue2 的核心思路是:

在初始化阶段,把 data 中的每一个属性都转换成带有 getter / setter 的响应式属性。

Vue2 官方响应式流程图#

Vue2 响应式流程图

Vue2 文档里明确提到:当你把一个普通对象传给 Vue 实例的 data 后,Vue 会遍历它的属性,并使用 Object.defineProperty 把这些属性转换成 getter / setter

2.1 Vue2 的核心流程#

大致可以简化成下面几步:

  1. 初始化时递归遍历 data
  2. 对每个属性通过 Object.defineProperty 定义 getset
  3. get 里收集依赖
  4. set 里派发更新
  5. 对应组件的 Watcher 重新执行,触发重新渲染

你可以把它理解成一个最简版本:

function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
// 收集依赖
return val
},
set(newVal) {
val = newVal
// 通知依赖更新
}
})
}

三、Vue2 的三个核心角色:Observer、Dep、Watcher#

Vue2 的响应式系统里,最关键的三个角色是:

3.1 Observer:把普通对象转成响应式对象#

Observer 会递归遍历对象,把每个属性都变成响应式。

也就是说,Vue2 在初始化时会“提前走一遍数据树”。

这也是 Vue2 的一个明显特征:

它更像“初始化时改造数据”,而不是“运行时动态代理对象”。

3.2 Dep:依赖收集器#

每个响应式属性内部都会关联一个 Dep,你可以把它理解成:

“这个属性都被谁依赖了”的名单。

当属性被读取时,如果当前有活跃的 Watcher,就把它收集进去。

3.3 Watcher:依赖这个数据的订阅者#

Watcher 可以理解成“副作用”或“观察者”:

  • 组件渲染 watcher
  • 用户自己写的 watcher
  • computed 背后的 watcher

当依赖的属性变化时,相关 watcher 会收到通知,然后重新执行。


四、Vue2 中依赖是怎么收集的?#

依赖收集发生在 getter 中。

4.1 为什么在 getter 收集?#

因为只有当某个属性被“读取”时,Vue 才知道:

“哦,当前这段逻辑依赖了这个属性。”

例如组件渲染函数执行时:

<template>
<div>{{ count }}</div>
</template>

渲染模板时会读取 count,这时 count 的 getter 会被触发,于是:

  1. 发现当前有一个正在执行的渲染 watcher
  2. 把这个 watcher 收集到 count 对应的 Dep 中

这就建立了“数据 -> watcher”的依赖关系。


五、Vue2 中更新是怎么触发的?#

更新触发发生在 setter 中。

当你执行:

this.count = this.count + 1

会触发 count 的 setter。setter 做的事情大致是:

  1. 更新内部值
  2. 调用 dep.notify()
  3. 遍历所有 watcher
  4. 通知它们重新执行

对于组件来说,最终就是重新走一次渲染流程,生成新的 Virtual DOM,再 patch 到真实 DOM。


六、Vue2 为什么有一些“响应式缺陷”?#

这是 Vue2 面试里最经典的一部分,因为它直接暴露了 Object.defineProperty 的局限。

6.1 无法监听对象属性的新增和删除#

例如:

vm.obj.newKey = 1

这个新属性在初始化时并不存在,因此 Vue2 没有机会为它定义 getter/setter,所以它不是响应式的。

因此 Vue2 里需要:

Vue.set(vm.obj, 'newKey', 1)

或者:

this.$set(this.obj, 'newKey', 1)

6.2 无法直接监听数组下标修改#

例如:

vm.list[1] = 'x'

Vue2 不能检测到。

6.3 无法直接监听数组长度修改#

例如:

vm.list.length = 2

也不是响应式的。

6.4 为什么数组还能“部分响应式”?#

因为 Vue2 对数组做了另外一套补丁:

  • 重写 push
  • 重写 pop
  • 重写 shift
  • 重写 unshift
  • 重写 splice
  • 重写 sort
  • 重写 reverse

也就是说,Vue2 并不是直接监听数组索引,而是劫持会修改数组的变异方法,在这些方法执行后手动通知更新。

所以在 Vue2 里常见正确写法是:

vm.list.splice(index, 1, newValue)

七、Vue2 的异步更新队列#

Vue2 不会每次数据变化都立即同步更新 DOM,而是会把更新放进一个队列里,在下一个事件循环 tick 中统一处理。

这样做的好处是:

  • 避免重复渲染
  • 多次数据变化只触发一次 DOM 更新
  • 减少性能浪费

这也是为什么在 Vue2 里经常要配合:

this.$nextTick(() => {
// 此时 DOM 已更新
})

八、Vue3 的响应式原理:Proxy + Reflect#

Vue3 最大的变化,是把响应式核心从 Object.defineProperty 换成了 Proxy

Vue3 响应式原理图#

Vue3 响应式原理图

Vue3 官方文档给出的核心伪代码大致是:

function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key)
}
})
}

这说明 Vue3 的核心思路变成了:

  1. 使用 Proxy 代理整个对象
  2. get trap 中收集依赖
  3. set trap 中触发更新

这相比 Vue2 更自然、更彻底。


九、Vue3 的核心角色:track、trigger、effect#

Vue3 不再以 Vue2 那种 Dep + Watcher 的概念暴露给外部,而是内部更偏向下面这几个核心原语:

  • track
  • trigger
  • effect
  • reactive
  • ref
  • computed

Vue3 effect / track / trigger 示意图#

Vue3 effect 依赖追踪示意图

9.1 effect:副作用函数#

effect 本质上就是:

一段依赖响应式数据的逻辑。

例如组件渲染、computed、watchEffect,本质上都和 effect 有关。

9.2 track:依赖收集#

当 effect 执行过程中读取了某个响应式属性,就会触发 track(target, key)

它做的事情是:

把“当前活跃 effect”收集到 target[key] 的依赖集合中。

9.3 trigger:派发更新#

当响应式属性被修改时,会触发 trigger(target, key)

它会找到依赖这个属性的 effect,并让它们重新执行或进入调度队列。


十、Vue3 的依赖结构为什么更通用?#

Vue3 官方文档里提到,一个非常关键的数据结构是:

WeakMap<target, Map<key, Set<effect>>>

可以这样理解:

  • WeakMap:按对象维度存依赖
  • Map:对象内部按属性维度存依赖
  • Set:一个属性可能被多个 effect 依赖

也就是:

target -> key -> effects

例如:

  • 某个响应式对象 user
  • 它的属性 name
  • 有两个 effect 依赖了 user.name

那么最终就是:

user -> name -> [effect1, effect2]

这套结构的扩展性和表达能力都比 Vue2 更强。


十一、Vue3 为什么能解决 Vue2 的很多限制?#

关键就在于 Proxy 是“代理整个对象”,而不是像 Vue2 那样只改造已有属性。

11.1 可以监听属性新增 / 删除#

因为 Proxy 可以拦截:

  • get
  • set
  • deleteProperty
  • has
  • ownKeys

所以对象新增属性、删除属性,都能被感知。

11.2 数组支持更完整#

Vue3 不需要像 Vue2 那样主要依赖“重写数组变异方法”来兜底。因为数组本质也是对象,索引访问、长度变化都能通过代理感知。

11.3 可以支持 Map / Set#

这是 Vue2 基本做不到的能力之一。

Vue3 的响应式系统可以扩展到:

  • Map
  • Set
  • WeakMap
  • WeakSet

这让响应式系统的通用性大幅提升。


十二、Vue3 的 ref 为什么不是 Proxy?#

这是一个非常容易被问到的细节。

Vue3 中:

  • reactive() 主要用于对象,底层依赖 Proxy
  • ref() 主要用于包装单个值,底层依赖带 getter/setter 的 .value

官方伪代码大致类似:

function ref(value) {
const refObject = {
get value() {
track(refObject, 'value')
return value
},
set value(newValue) {
value = newValue
trigger(refObject, 'value')
}
}
return refObject
}

所以你可以把它理解成:

  • 对象响应式:Proxy
  • 单值响应式:getter / setter 包装

十三、Vue2 和 Vue3 响应式实现的本质差异#

对比项Vue2Vue3
核心实现Object.definePropertyProxy
依赖模型Dep + Watchertrack + trigger + effect
属性新增删除无法天然监听可以监听
数组索引 / length处理不完整更完整
Map / Set 支持基本不支持支持
初始化成本需要递归遍历数据劫持按访问时代理,能力更灵活
调试和抽象能力偏组件内部实现可抽象为独立响应式系统

十四、Vue3 为什么说响应式系统“更现代”?#

这里的“更现代”不只是因为换了 ES6 API,而是因为它的抽象层次更高。

Vue3 的响应式系统已经不只是“服务模板渲染”:

  • reactive
  • ref
  • computed
  • watch
  • watchEffect

这些 API 本质上都建立在同一个响应式核心之上。

也就是说,Vue3 把响应式系统从“框架内部黑盒能力”提升成了“可直接编程的底层能力”。

这就是为什么 Vue3 的组合式 API 和逻辑复用能力比 Vue2 强很多。


十五、面试里怎么回答“Vue2 和 Vue3 响应式原理”?#

如果在面试里被问到,我建议按这个结构回答:

第一步:先说共同目标#

两者本质都是为了实现:

当数据变化时,依赖该数据的视图或副作用自动更新。

第二步:再说 Vue2#

  • Vue2 会在初始化时递归遍历 data
  • 通过 Object.defineProperty 劫持每个属性的 getter/setter
  • 在 getter 中依赖收集
  • 在 setter 中派发更新
  • 通过 DepWatcher 建立依赖关系

第三步:再说 Vue3#

  • Vue3 用 Proxy 代理整个对象
  • gettrack
  • settrigger
  • 通过 effect 表示副作用
  • 底层依赖结构是 WeakMap -> Map -> Set

第四步:最后总结差异#

Vue3 相比 Vue2 最大优势在于:

  • 能监听新增和删除属性
  • 对数组、Map、Set 支持更完整
  • 响应式系统更通用,可独立抽象

如果你能按这个层次回答,已经算是比较完整的中高级答案。


十六、总结#

Vue2 和 Vue3 的响应式原理,核心并不神秘,本质就是:

  1. 在数据被读取时记录依赖
  2. 在数据被修改时通知依赖重新执行

差别在于它们拦截数据访问的方式不同:

  • Vue2 用 Object.defineProperty
  • Vue3 用 Proxy

而这种底层能力的升级,直接带来了 Vue3 在:

  • 可维护性
  • 可扩展性
  • 数据结构支持
  • API 抽象能力

上的整体进化。

所以真正精准的总结应该是:

Vue2 是“基于 getter/setter 的组件级响应式系统”,Vue3 则是“基于 Proxy 的通用响应式运行时”。

Vue2 和 Vue3 实现响应式的原理
https://fuwari.vercel.app/posts/vue2-vs-vue3-reactivity-principles/
Author
Owen
Published at
2026-05-29
License
CC BY-NC-SA 4.0