初次接触 Vue 响应式原理时,不少开发者常被Observer、Dep、Watcher、defineReactive 这些概念绕晕。 说到底,它们都指向同一个核心问题——当数据变化时,Vue 如何精准地知道谁依赖了它,并立即通知到这些依赖方。这正是 Vue 2 中 Object 变化侦测的核心机制。
Observer、Dep、Watcher、defineReactive 这些概念绕晕。说到底,它们都指向同一个核心问题——当数据变化时,Vue 如何精准地知道谁依赖了它,并立即通知到这些依赖方。这正是 Vue 2 中 Object 变化侦测的核心机制。

长期稳定更新的攒劲资源: >>>点此立即查看<<<
当一个对象属性发生变化时,Vue 如何知道哪些地方使用了它,并触发更新?
Vue 采用声明式渲染。模板中写一个 h1 标签,用 {{ name }} 动态插值,Vue 会自动根据 name 的值生成对应 DOM。但问题随之而来:当 name 改变后,Vue 凭什么知道页面需要更新?这正是变化侦测要解决的核心任务。
变化侦测大体有两种思路。一种是“拉”——状态变化后,框架只知道“可能有东西变了”,于是重新计算一遍,通过比较找出真正需要更新的地方。React 的虚拟 DOM diff 就是这种思路。另一种是“推”——状态变化的那一刻,主动通知所有依赖它的地方。Vue 2 的响应式系统走的是后者。
Vue 的优势在于:它不等最后才去猜测哪个地方变了,而是在数据被读取时就记录下“谁用过我”,等数据被修改时直接通知这些使用者。一句话概括:
读取时收集依赖,修改时触发依赖。
在 Vue 2 中,对象属性的变化主要依靠 Object.defineProperty 实现。它可以将一个普通属性改造成带有 getter 和 setter 的属性:
function defineReactive(data, key, val) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
}
})
}
这样一来,读取 data[key] 时触发 get,修改时触发 set。但这还不够——仅知道属性被读被改,还无法完成完整的响应式。真正的难点在于:读取时需要知道是谁在读,修改时需要通知这些读取者更新。这就引出了 Vue 响应式系统中的第一个核心角色:Dep。
可以把 Dep 理解成一个订阅中心。每个响应式属性都拥有一个自己的 Dep,谁读取了它就把谁收集进来,等属性变化时再通知所有订阅者。简化版代码如下:
class Dep {
constructor() {
this.subs = []
}
depend() {
if (Dep.target) {
this.subs.push(Dep.target)
}
}
notify() {
this.subs.forEach(sub => sub.update())
}
}
再把它放回 defineReactive:
function defineReactive(data, key, val) {
const dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
dep.depend()
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
dep.notify()
}
})
}
逻辑一下子清晰了:
get -> dep.depend() -> 收集依赖
set -> dep.notify() -> 通知依赖
但问题又来了:Dep.target 是谁?答案是:Watcher。
Vue 不会直接把 DOM、模板或者用户回调塞进 Dep,而是统一抽象成一个对象:Watcher。可以把 Watcher 理解为“响应式系统里的订阅者”。例如:
vm.$watch('user.name', function(newVal, oldVal) {
console.log(newVal, oldVal)
})
背后就会创建一个 Watcher。它的任务很明确:读取 user.name → 触发 getter → 在 getter 中把自己收集进 Dep → 当数据变化时执行自己的 update → 最终调用用户传入的回调。
简化后的 Watcher 可以这样理解:
class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm
this.getter = parsePath(expOrFn)
this.cb = cb
this.value = this.get()
}
get() {
Dep.target = this
const value = this.getter(this.vm)
Dep.target = null
return value
}
update() {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
这里最关键的就是 get() 方法。它先把当前 Watcher 挂到全局位置:Dep.target = this,然后再去读取数据。读取数据时触发 getter,getter 里执行 dep.depend(),于是这个 Watcher 就被收进了对应属性的 Dep 里。
这就是 Vue 2 依赖收集的核心技巧:Watcher 先把自己挂到全局目标上,再主动读取数据。数据的 getter 被触发后,就能反过来把这个 Watcher 收集起来。
当我们写 vm.$watch('a.b.c', callback) 时,Vue 需要根据字符串 'a.b.c' 读到真正的值。简化实现如下:
function parsePath(path) {
const segments = path.split('.')
return function(obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
它做的事很简单:把 'a.b.c' 切割成 ['a','b','c'],然后一层一层读取。注意,这个读取过程会连续触发每一层属性的 getter,因此 Watcher 不只是订阅了最后的 c,它在读取链路上经过的属性也可能参与依赖收集。
前面 defineReactive 只能处理单个属性,但 Vue 的 data 往往是一个完整对象:
data() {
return {
user: {
name: 'Tom',
age: 18
}
}
}
Vue 需要递归地把每个属性都变成响应式属性,这就是 Observer 的作用:
class Observer {
constructor(value) {
this.value = value
if (!Array.isArray(value)) {
this.walk(value)
}
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
同时,defineReactive 里面还要递归处理子对象:
function defineReactive(data, key, val) {
if (typeof val === 'object' && val !== null) {
new Observer(val)
}
const dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
dep.depend()
return val
},
set(newVal) {
if (newVal === val) return
if (typeof newVal === 'object' && newVal !== null) {
new Observer(newVal)
}
val = newVal
dep.notify()
}
})
}
一个普通对象经过 Observer 处理后,内部的每个属性都会被转换成 getter/setter。可以这样理解:
Observer:负责把对象加工成响应式对象;defineReactive:负责把单个属性变成 getter/setter;Dep:负责保存这个属性的依赖;Watcher:负责在数据变化后执行更新逻辑。假设模板里用了 ,Vue 会为这个组件创建一个渲染 {{ user.name }}
Watcher。
第一次渲染时,流程是:
Watcher 开始渲染
↓
读取 user.name
↓
触发 user 和 name 的 getter
↓
getter 调用 dep.depend()
↓
当前 Watcher 被收集进 Dep
之后当我们执行 this.user.name = 'Jerry' 时,流程变成:
触发 name 的 setter
↓
setter 调用 dep.notify()
↓
Dep 通知所有 Watcher
↓
Watcher.update()
↓
组件重新渲染
↓
生成新的虚拟 DOM
↓
diff 后更新真实 DOM
这就是 Vue 2 对象响应式的主链路。
Object.defineProperty 有一个天然限制:它只能拦截已经存在的属性。比如初始化时 data 返回 { user: {} },后面再写 this.user.name = 'Tom',这个 name 是后来新增的,初始化阶段 Vue 没有见过它,也就没有机会给它设置 getter/setter。所以 Vue 2 无法自动侦测新增属性。
删除属性也一样:delete this.user.name 不会触发任何已定义的 setter,因此 Vue 2 也无法直接感知。这就是为什么 Vue 2 需要提供 Vue.set()、Vue.delete() 或实例方法 this.$set()、this.$delete()。它们的本质就是绕过 Object.defineProperty 的限制,手动补上响应式处理和依赖通知。
既然 Vue 已经能精确知道哪个数据变了,那为什么 Vue 2 还要用虚拟 DOM?因为粒度太细也有成本。如果每个数据都直接绑定到具体 DOM 节点,依赖关系会非常多,内存和维护成本都会上升。所以 Vue 2 选择了一个折中方案:数据变化时通知组件,组件内部再通过虚拟 DOM diff 找出真正需要更新的 DOM。
也就是说,Vue 2 并没有完全放弃“推”,而是把更新粒度从“具体 DOM 节点”提升到了“组件级别”。这样既保留了响应式依赖追踪的优势,又避免了过细粒度带来的巨大依赖开销。
Vue 2 中 Object 的变化侦测,可以压缩成一句话:用 Object.defineProperty 拦截属性读写;读取时收集 Watcher,修改时通知 Watcher。
几个核心角色分别是:
Observer
负责遍历对象,把属性转换成 getter/setter。
defineReactive
负责处理单个属性,让它具备响应式能力。
Dep
负责保存依赖,相当于每个属性自己的订阅列表。
Watcher
是真正的订阅者。它读取数据时被收集,数据变化时被通知。
完整流程是:
初始化 data
↓
Observer 遍历对象
↓
defineReactive 转换属性
↓
Watcher 读取数据
↓
getter 收集依赖
↓
数据被修改
↓
setter 触发通知
↓
Dep 通知 Watcher
↓
Watcher 执行更新
这就是 Vue 2 响应式系统的基本骨架。理解这条链路后,再去看 Vue 源码里的 Observer、Dep、Watcher,就不会觉得它们是几个孤立的类,而是一套完整的订阅发布系统:数据负责暴露变化,Dep 负责管理依赖,Watcher 负责响应变化,视图更新只是 Watcher 被触发后的结果。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述