從源碼角度帶你深入理解Vue依賴收集的核心
在 Vue 的響應式系統(tǒng)中,“依賴收集” 是貫穿整個數(shù)據(jù)驅(qū)動視圖的核心環(huán)節(jié)。很多開發(fā)者日常使用data、computed、watch時只知其然,卻不知其所以然 —— 為什么修改數(shù)據(jù)視圖會自動更新?為什么computed能精準緩存?這一切的背后,都是依賴收集機制在起作用。
本文將從 Vue 2 的源碼出發(fā),層層拆解依賴收集的完整流程,帶你從 “使用層” 走向 “原理層”,真正理解 Vue 響應式的底層邏輯。
一、先搞懂:什么是 “依賴”
在開始源碼分析前,我們先明確核心概念:
- 依賴:本質(zhì)上是 “使用了某個響應式數(shù)據(jù)的執(zhí)行函數(shù)”,比如渲染組件的
render函數(shù)、computed的計算函數(shù)、watch的回調(diào)函數(shù)。 - 依賴收集:在響應式數(shù)據(jù)被讀取時,記錄下 “哪些函數(shù)依賴了這個數(shù)據(jù)”;當數(shù)據(jù)被修改時,找到這些記錄的函數(shù)并執(zhí)行,最終實現(xiàn) “數(shù)據(jù)變 → 視圖更”。
簡單來說,依賴收集的核心目標是:建立 “響應式數(shù)據(jù)” 與 “使用數(shù)據(jù)的函數(shù)” 之間的映射關系。
二、核心角色:依賴收集的 3 個關鍵模塊
Vue 2 的依賴收集主要依賴三個核心模塊,我們先認識它們:
| 模塊 | 作用 | 核心源碼位置 |
|---|---|---|
| Observer | 將普通對象 / 數(shù)組轉(zhuǎn)為響應式(給屬性添加 get/set) | src/core/observer/index.js |
| Dep | 依賴管理器:存儲某個響應式數(shù)據(jù)的所有依賴 | src/core/observer/dep.js |
| Watcher | 依賴的載體:封裝需要執(zhí)行的函數(shù)(如 render、computed) | src/core/observer/watcher.js |
三者的關系可以總結(jié)為:
Observer 給數(shù)據(jù)加 get/set 鉤子 → 讀取數(shù)據(jù)時觸發(fā) get,通過Dep收集Watcher → 修改數(shù)據(jù)時觸發(fā) set,通過Dep通知所有Watcher執(zhí)行。
三、源碼拆解:依賴收集的完整流程
3.1 第一步:響應式數(shù)據(jù)的初始化(Observer)
首先,Vue 會通過Observer類將data中的數(shù)據(jù)轉(zhuǎn)為響應式,核心是給每個屬性定義getter/setter。
核心源碼(簡化版):
// src/core/observer/index.js
class Observer {
constructor(value) {
this.value = value
this.dep = new Dep() // 給對象/數(shù)組本身創(chuàng)建Dep
def(value, '__ob__', this) // 給數(shù)據(jù)添加__ob__屬性,標記為響應式
if (Array.isArray(value)) {
// 處理數(shù)組的響應式(重寫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])
}
}
// 遍歷數(shù)組,給每個元素做響應式處理
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
// 遞歸處理子屬性,保證深層數(shù)據(jù)也是響應式
let childOb = !shallow && observe(val)
// 定義新的getter/setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// 讀取屬性時觸發(fā):依賴收集的入口
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
// 關鍵:如果當前有活躍的Watcher,就收集依賴
if (Dep.target) {
dep.depend() // 1. 讓Dep記錄當前Watcher
if (childOb) {
// 2. 給對象/數(shù)組本身也收集依賴(處理數(shù)組/對象整體變更)
childOb.dep.depend()
if (Array.isArray(value)) {
// 3. 數(shù)組特殊處理:遍歷子元素收集依賴
dependArray(value)
}
}
}
return value
},
// 修改屬性時觸發(fā):通知依賴更新
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()
}
})
}
核心要點:
- 每個響應式屬性都會創(chuàng)建一個
Dep實例,專屬管理該屬性的依賴; getter中觸發(fā)依賴收集,setter中觸發(fā)依賴更新;- 不僅處理單個屬性,還會遞歸處理子對象 / 數(shù)組,保證深層響應式。
3.2 第二步:依賴管理器(Dep)
Dep是依賴的 “容器”,核心作用是存儲和管理某個數(shù)據(jù)的所有Watcher,提供depend(收集)和notify(通知)兩個核心方法。
核心源碼(簡化版):
// src/core/observer/dep.js
export default class Dep {
static target: ?Watcher; // 靜態(tài)屬性,存儲當前活躍的Watcher
id: number; // 唯一標識
subs: Array<Watcher>; // 存儲依賴的Watcher數(shù)組
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) {
// 調(diào)用當前Watcher的addDep方法,雙向綁定
Dep.target.addDep(this)
}
}
// 核心:通知所有Watcher更新
notify() {
// 復制一份依賴列表,避免更新過程中列表變化
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
// 調(diào)用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是全局唯一的,始終指向 “當前正在執(zhí)行的 Watcher”;depend()方法不是直接添加 Watcher,而是調(diào)用Watcher.addDep(),實現(xiàn) Dep 和 Watcher 的雙向記錄(避免重復收集);- 用棧結(jié)構(gòu)
targetStack處理嵌套場景(比如組件嵌套、computed 嵌套)。
3.3 第三步:依賴載體(Watcher)
Watcher是 “依賴” 的具體載體,每個 Watcher 對應一個需要執(zhí)行的函數(shù)(比如組件的render函數(shù)、computed的計算函數(shù))。
核心源碼(簡化版):
// 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 // 深度監(jiān)聽用
this.sync = !!options.sync // 同步更新用
} else {
this.lazy = this.deep = this.sync = false
}
this.cb = cb // 更新回調(diào)
this.id = uid++ // 唯一標識
this.deps = [] // 存儲當前Watcher依賴的Dep
this.newDeps = [] // 臨時存儲新依賴(用于依賴清理)
this.depIds = new Set() // 去重
this.newDepIds = new Set()
// 解析表達式/函數(shù),得到最終要執(zhí)行的函數(shù)
this.getter = typeof expOrFn === 'function' ? expOrFn : parsePath(expOrFn)
// 非lazy模式(比如render、watch)立即執(zhí)行g(shù)et,觸發(fā)依賴收集
this.value = this.lazy ? undefined : this.get()
}
// 核心:執(zhí)行g(shù)etter并收集依賴
get() {
// 1. 將當前Watcher入棧,設置為Dep.target
pushTarget(this)
let value
const vm = this.vm
try {
// 2. 執(zhí)行g(shù)etter(比如render函數(shù)),觸發(fā)數(shù)據(jù)的getter
// 此時數(shù)據(jù)的getter會檢測到Dep.target,從而收集當前Watcher
value = this.getter.call(vm, vm)
} catch (e) {
// 錯誤處理
} finally {
// 3. 深度監(jiān)聽處理
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)
}
}
}
// 清理無用依賴(比如數(shù)據(jù)從視圖中移除后,不再監(jiān)聽)
cleanupDeps() {
// 省略清理邏輯...
}
// 核心:響應式數(shù)據(jù)更新時,觸發(fā)Watcher更新
update() {
if (this.lazy) {
// computed:標記為臟值,下次訪問時重新計算
this.dirty = true
} else if (this.sync) {
// 同步更新:立即執(zhí)行run
this.run()
} else {
// 異步更新(Vue默認):加入隊列,批量更新
queueWatcher(this)
}
}
// 執(zhí)行g(shù)etter并觸發(fā)回調(diào)
run() {
const value = this.get()
if (value !== this.value || this.deep) {
const oldValue = this.value
this.value = value
// 執(zhí)行回調(diào)(比如watch的回調(diào)函數(shù))
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()是觸發(fā)依賴收集的關鍵:先將自身設為Dep.target,再執(zhí)行getter(比如render函數(shù)),此時render中用到的所有響應式數(shù)據(jù)都會觸發(fā)getter,從而收集當前 Watcher;
不同類型的 Watcher 有不同的更新策略:
- 渲染 Watcher(render):異步更新,加入隊列批量執(zhí)行;
- 計算 Watcher(computed):懶更新(
lazy: true),只有訪問時才重新計算; - 偵聽 Watcher(watch):可配置同步 / 異步,支持深度監(jiān)聽。
四、特殊場景的依賴處理
4.1 數(shù)組的依賴收集
數(shù)組的響應式處理和對象不同(因為數(shù)組的索引無法被Object.defineProperty攔截),Vue 重寫了push、pop、splice等 7 個數(shù)組方法,核心邏輯:
// 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) {
// 執(zhí)行原方法
const result = original.apply(this, args)
// 獲取數(shù)組的__ob__(Observer實例)
const ob = this.__ob__
// 處理新增元素(push/unshift/splice),轉(zhuǎn)為響應式
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
})
})
核心:數(shù)組的依賴收集在數(shù)組本身的__ob__.dep中,修改數(shù)組時調(diào)用ob.dep.notify()觸發(fā)更新。
4.2 computed 的依賴收集
computed的 Watcher 是 “懶 Watcher”(lazy: true),特點:
- 初始化時不立即執(zhí)行
get,只有首次訪問時才觸發(fā); - 依賴的數(shù)據(jù)更新時,只標記
dirty: true,不立即重新計算; - 下次訪問
computed屬性時,才重新計算并緩存結(jié)果。
核心邏輯:
// 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()
}
// 收集渲染W(wǎng)atcher到computed的依賴中
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
五、常見問題與面試考點
為什么 Vue 不能檢測數(shù)組索引和長度的變化
- 數(shù)組索引的
get/set雖然能被Object.defineProperty攔截,但考慮到性能成本(數(shù)組元素可能很多),Vue 放棄了這種方式; - 數(shù)組長度的
set也無法被有效攔截,且修改長度的場景較少; - 解決方案:使用 Vue 提供的變異方法(push/splice 等)或
Vue.set。
為什么修改對象的新屬性視圖不更新
- 因為對象初始化時,只有已定義的屬性被添加了
get/set,新屬性沒有; - 解決方案:使用
Vue.set(obj, key, value)或this.$set,本質(zhì)是給新屬性添加get/set并觸發(fā)依賴更新。
依賴收集為什么要雙向記錄(Dep→Watcher 和 Watcher→Dep)
- 避免重復收集:通過
depIds和newDepIds去重; - 方便依賴清理:組件銷毀時,Watcher 可以遍歷自己的
deps,從 Dep 中移除自身; - 支持依賴更新:Watcher 可以通過
deps重新收集依賴(比如computed的depend方法)。
六、總結(jié)
Vue 的依賴收集機制是響應式系統(tǒng)的靈魂,核心可以總結(jié)為 3 點:
- 核心鏈路:
Observer給數(shù)據(jù)加get/set→ 讀取數(shù)據(jù)時Dep收集Watcher→ 修改數(shù)據(jù)時Dep通知Watcher執(zhí)行 → 視圖更新; - 核心角色:
Observer(響應式標記)、Dep(依賴容器)、Watcher(依賴載體)三者協(xié)同工作; - 性能優(yōu)化:通過懶更新(computed)、異步隊列(渲染 Watcher)、依賴清理等方式,保證響應式的高效性。
理解依賴收集,不僅能幫你解決日常開發(fā)中的響應式問題,更能讓你從底層理解 Vue 的設計思想。
以上就是從源碼角度帶你深入理解Vue依賴收集的核心的詳細內(nèi)容,更多關于Vue依賴收集的資料請關注腳本之家其它相關文章!
相關文章
vue element ui validate 主動觸發(fā)錯誤提示操作
這篇文章主要介紹了vue element ui validate 主動觸發(fā)錯誤提示操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09

