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

