在 Vue 的响应式系统中,“依赖收集” 是贯穿整个数据驱动视图的核心环节。很多开发者日常使用data、computed、watch时只知其然,却不知其所以然 —— 为什么修改数据视图会自动更新?为什么computed能精准缓存?这一切的背后,都是依赖收集机制在起作用。
本文将从 Vue 2 的源码出发,层层拆解依赖收集的完整流程,带你从 “使用层” 走向 “原理层”,真正理解 Vue 响应式的底层逻辑。
一、先搞懂:什么是 “依赖”?
在开始源码分析前,我们先明确核心概念:
- 依赖:本质上是 “使用了某个响应式数据的执行函数”,比如渲染组件的
render函数、computed的计算函数、watch的回调函数。 - 依赖收集:在响应式数据被读取时,记录下 “哪些函数依赖了这个数据”;当数据被修改时,找到这些记录的函数并执行,最终实现 “数据变 → 视图更”。
简单来说,依赖收集的核心目标是:建立 “响应式数据” 与 “使用数据的函数” 之间的映射关系。
二、核心角色:依赖收集的 3 个关键模块
Vue 2 的依赖收集主要依赖三个核心模块,我们先认识它们:
表格
| 模块 | 作用 | 核心源码位置 |
|---|---|---|
Observer | 将普通对象 / 数组转为响应式(给属性添加 get/set) | src/core/observer/index.js |
Dep | 依赖管理器:存储某个响应式数据的所有依赖 | src/core/observer/dep.js |
Watcher | 依赖的载体:封装需要执行的函数(如 render、computed) | src/core/observer/watcher.js |
三者的关系可以总结为:
Observer给数据加 get/set 钩子 → 读取数据时触发 get,通过Dep收集Watcher→ 修改数据时触发 set,通过Dep通知所有Watcher执行。
三、源码拆解:依赖收集的完整流程
3.1 第一步:响应式数据的初始化(Observer)
首先,Vue 会通过Observer类将data中的数据转为响应式,核心是给每个属性定义getter/setter。
核心源码(简化版):
javascript
运行
// src/core/observer/index.js
class Observer {
constructor(value) {
this.value = value
this.dep = new Dep() // 给对象/数组本身创建Dep
def(value, '__ob__', this) // 给数据添加__ob__属性,标记为响应式
if (Array.isArray(value)) {
// 处理数组的响应式(重写push/pop等方法)
this.observeArray(value)
} else {
// 处理对象的响应式:遍历属性并定义get/set
this.walk(value)
}
}
// 遍历对象属性,定义响应式
walk(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
// 遍历数组,给每个元素做响应式处理
observeArray(items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
// 核心:给单个属性定义get/set
export function defineReactive(
obj,
key,
val,
customSetter,
shallow
) {
// 每个响应式属性都有一个专属的Dep实例
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// 保留原有的get/set
const getter = property && property.get
const setter = property && property.set
// 递归处理子属性,保证深层数据也是响应式
let childOb = !shallow && observe(val)
// 定义新的getter/setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// 读取属性时触发:依赖收集的入口
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
// 关键:如果当前有活跃的Watcher,就收集依赖
if (Dep.target) {
dep.depend() // 1. 让Dep记录当前Watcher
if (childOb) {
// 2. 给对象/数组本身也收集依赖(处理数组/对象整体变更)
childOb.dep.depend()
if (Array.isArray(value)) {
// 3. 数组特殊处理:遍历子元素收集依赖
dependArray(value)
}
}
}
return value
},
// 修改属性时触发:通知依赖更新
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
// 新旧值相同则不处理
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 新值也要做响应式处理
childOb = !shallow && observe(newVal)
// 关键:通知所有依赖更新
dep.notify()
}
})
}
核心要点:
- 每个响应式属性都会创建一个
Dep实例,专属管理该属性的依赖; getter中触发依赖收集,setter中触发依赖更新;- 不仅处理单个属性,还会递归处理子对象 / 数组,保证深层响应式。
3.2 第二步:依赖管理器(Dep)
Dep是依赖的 “容器”,核心作用是存储和管理某个数据的所有Watcher,提供depend(收集)和notify(通知)两个核心方法。
核心源码(简化版):
javascript
运行
// src/core/observer/dep.js
export default class Dep {
static target: ?Watcher; // 静态属性,存储当前活跃的Watcher
id: number; // 唯一标识
subs: Array<Watcher>; // 存储依赖的Watcher数组
constructor() {
this.id = uid++
this.subs = []
}
// 添加一个Watcher到依赖列表
addSub(sub: Watcher) {
this.subs.push(sub)
}
// 移除一个Watcher
removeSub(sub: Watcher) {
remove(this.subs, sub)
}
// 核心:收集依赖(让Dep和Watcher互相记录)
depend() {
if (Dep.target) {
// 调用当前Watcher的addDep方法,双向绑定
Dep.target.addDep(this)
}
}
// 核心:通知所有Watcher更新
notify() {
// 复制一份依赖列表,避免更新过程中列表变化
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
// 调用Watcher的update方法
subs[i].update()
}
}
}
// 全局唯一的Dep.target栈(处理嵌套Watcher,比如computed嵌套)
Dep.target = null
const targetStack = []
// 入栈:设置当前活跃的Watcher
export function pushTarget(target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
// 出栈:恢复上一个Watcher
export function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
核心要点:
Dep.target是全局唯一的,始终指向 “当前正在执行的 Watcher”;depend()方法不是直接添加 Watcher,而是调用Watcher.addDep(),实现 Dep 和 Watcher 的双向记录(避免重复收集);- 用栈结构
targetStack处理嵌套场景(比如组件嵌套、computed 嵌套)。
3.3 第三步:依赖载体(Watcher)
Watcher是 “依赖” 的具体载体,每个 Watcher 对应一个需要执行的函数(比如组件的render函数、computed的计算函数)。
核心源码(简化版):
javascript
运行
// src/core/observer/watcher.js
export default class Watcher {
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// 处理配置项(比如lazy、deep、sync)
if (options) {
this.lazy = !!options.lazy // computed用
this.deep = !!options.deep // 深度监听用
this.sync = !!options.sync // 同步更新用
} else {
this.lazy = this.deep = this.sync = false
}
this.cb = cb // 更新回调
this.id = uid++ // 唯一标识
this.deps = [] // 存储当前Watcher依赖的Dep
this.newDeps = [] // 临时存储新依赖(用于依赖清理)
this.depIds = new Set() // 去重
this.newDepIds = new Set()
// 解析表达式/函数,得到最终要执行的函数
this.getter = typeof expOrFn === 'function' ? expOrFn : parsePath(expOrFn)
// 非lazy模式(比如render、watch)立即执行get,触发依赖收集
this.value = this.lazy ? undefined : this.get()
}
// 核心:执行getter并收集依赖
get() {
// 1. 将当前Watcher入栈,设置为Dep.target
pushTarget(this)
let value
const vm = this.vm
try {
// 2. 执行getter(比如render函数),触发数据的getter
// 此时数据的getter会检测到Dep.target,从而收集当前Watcher
value = this.getter.call(vm, vm)
} catch (e) {
// 错误处理
} finally {
// 3. 深度监听处理
if (this.deep) {
traverse(value)
}
// 4. 出栈,恢复Dep.target
popTarget()
// 5. 清理无用的依赖
this.cleanupDeps()
}
return value
}
// 核心:添加Dep到Watcher(与Dep.depend()配合)
addDep(dep: Dep) {
const id = dep.id
// 避免重复收集
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
// 让Dep也记录当前Watcher
dep.addSub(this)
}
}
}
// 清理无用依赖(比如数据从视图中移除后,不再监听)
cleanupDeps() {
// 省略清理逻辑...
}
// 核心:响应式数据更新时,触发Watcher更新
update() {
if (this.lazy) {
// computed:标记为脏值,下次访问时重新计算
this.dirty = true
} else if (this.sync) {
// 同步更新:立即执行run
this.run()
} else {
// 异步更新(Vue默认):加入队列,批量更新
queueWatcher(this)
}
}
// 执行getter并触发回调
run() {
const value = this.get()
if (value !== this.value || this.deep) {
const oldValue = this.value
this.value = value
// 执行回调(比如watch的回调函数)
this.cb.call(this.vm, value, oldValue)
}
}
// computed专用:计算并返回最新值
evaluate() {
this.value = this.get()
this.dirty = false
}
// 重新收集依赖
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
核心要点:
-
Watcher.get()是触发依赖收集的关键:先将自身设为Dep.target,再执行getter(比如render函数),此时render中用到的所有响应式数据都会触发getter,从而收集当前 Watcher; -
不同类型的 Watcher 有不同的更新策略:
- 渲染 Watcher(render):异步更新,加入队列批量执行;
- 计算 Watcher(computed):懒更新(
lazy: true),只有访问时才重新计算; - 侦听 Watcher(watch):可配置同步 / 异步,支持深度监听。
3.4 第四步:完整流程梳理(以组件渲染为例)
结合上面的源码,我们用流程图梳理组件渲染时的依赖收集完整流程:
预览
查看代码
组件初始化
创建渲染Watcher
生成失败,请重试
graph TD
A[组件初始化] --> B[创建渲染Watcher]
B --> C[执行Watcher.get()]
C --> D[pushTarget:设置Dep.target为当前Watcher]
D --> E[执行render函数]
E --> F[读取响应式数据,触发getter]
F --> G[Dep.depend():收集依赖]
G --> H[Watcher.addDep():双向绑定Dep和Watcher]
H --> I[render执行完成]
I --> J[popTarget:恢复Dep.target]
J --> K[依赖收集完成:数据→Dep→Watcher映射建立]
L[修改响应式数据] --> M[触发setter]
M --> N[Dep.notify():通知所有Watcher]
N --> O[Watcher.update():执行更新]
O --> P[重新执行render,更新视图]
组件初始化
创建渲染Watcher
生成失败,请重试
豆包
你的 AI 助手,助力每日工作学习
四、特殊场景的依赖处理
4.1 数组的依赖收集
数组的响应式处理和对象不同(因为数组的索引无法被Object.defineProperty拦截),Vue 重写了push、pop、splice等 7 个数组方法,核心逻辑:
javascript
运行
// src/core/observer/array.js
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// 重写的7个方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// 保留原方法
const original = arrayProto[method]
def(arrayMethods, method, function mutator(...args) {
// 执行原方法
const result = original.apply(this, args)
// 获取数组的__ob__(Observer实例)
const ob = this.__ob__
// 处理新增元素(push/unshift/splice),转为响应式
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// 关键:通知依赖更新
ob.dep.notify()
return result
})
})
核心:数组的依赖收集在数组本身的__ob__.dep中,修改数组时调用ob.dep.notify()触发更新。
4.2 computed 的依赖收集
computed的 Watcher 是 “懒 Watcher”(lazy: true),特点:
- 初始化时不立即执行
get,只有首次访问时才触发; - 依赖的数据更新时,只标记
dirty: true,不立即重新计算; - 下次访问
computed属性时,才重新计算并缓存结果。
核心逻辑:
javascript
运行
// src/core/instance/state.js
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
// 只有脏值时才重新计算
watcher.evaluate()
}
// 收集渲染Watcher到computed的依赖中
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
五、常见问题与面试考点
5.1 为什么 Vue 不能检测数组索引和长度的变化?
- 数组索引的
get/set虽然能被Object.defineProperty拦截,但考虑到性能成本(数组元素可能很多),Vue 放弃了这种方式; - 数组长度的
set也无法被有效拦截,且修改长度的场景较少; - 解决方案:使用 Vue 提供的变异方法(push/splice 等)或
Vue.set。
5.2 为什么修改对象的新属性视图不更新?
- 因为对象初始化时,只有已定义的属性被添加了
get/set,新属性没有; - 解决方案:使用
Vue.set(obj, key, value)或this.$set,本质是给新属性添加get/set并触发依赖更新。
5.3 依赖收集为什么要双向记录(Dep→Watcher 和 Watcher→Dep)?
- 避免重复收集:通过
depIds和newDepIds去重; - 方便依赖清理:组件销毁时,Watcher 可以遍历自己的
deps,从 Dep 中移除自身; - 支持依赖更新:Watcher 可以通过
deps重新收集依赖(比如computed的depend方法)。
六、总结
Vue 的依赖收集机制是响应式系统的灵魂,核心可以总结为 3 点:
- 核心链路:
Observer给数据加get/set→ 读取数据时Dep收集Watcher→ 修改数据时Dep通知Watcher执行 → 视图更新; - 核心角色:
Observer(响应式标记)、Dep(依赖容器)、Watcher(依赖载体)三者协同工作; - 性能优化:通过懒更新(computed)、异步队列(渲染 Watcher)、依赖清理等方式,保证响应式的高效性。
理解依赖收集,不仅能帮你解决日常开发中的响应式问题,更能让你从底层理解 Vue 的设计思想。希望本文能让你对 Vue 的响应式系统有更深入的认识~