文章目錄
- 引言
- 數據劫持
- 收集依賴
- 數組處理
- 渲染watcher
- vue3中的響應式
引言
vue的設計思想是數據雙向綁定、數據與UI自動同步,即數據驅動視圖。
為什么會這樣呢?這就不得不提vue的響應式原理了,在使用vue的過程中,我被vue的響應式設計深深著迷 ,下面我們就從源碼的角度,來分析一下vue是如何實現響應式原理的。
在vue2中,主要分為三個過程:
- 數據劫持:Vue 會遍歷組件實例的所有屬性,并使用
Object.defineProperty
將這些屬性轉換為 getter/setter 形式。這樣做的目的是為了追蹤依賴以及觸發更新。 - 依賴收集:當渲染函數執行時,如果訪問了響應式數據,那么這個訪問會被記錄下來,形成一個“依賴”。這意味著哪些視圖或計算屬性依賴于當前的數據。
- 視圖更新:一旦某個響應式數據發生改變(即調用了 setter),Vue 就會通知所有依賴于該數據的視圖進行重新渲染。
數據劫持
Vue
使用Object.defineProperty
來進行數據劫持。
Object.defineProperty
是 JavaScript 中的一個內置方法,它允許開發者在一個對象上定義新的屬性或修改現有屬性,并配置這些屬性的特性。
Object.defineProperty(obj, prop, descriptor);
//obj: 要在其上定義屬性的對象。
//prop: 要定義或修改的屬性名稱。
//descriptor: 將被定義或修改的屬性描述符。
屬性描述符(Descriptor)
descriptor
參數是一個對象,它可以包含以下幾種鍵:
數據描述符
- value: 屬性對應的值,默認為
undefined
。 - writable: 如果為
false
,則該屬性的值不能被改變,默認為false
。 - enumerable: 如果為
true
,則該屬性會出現在對象的屬性枚舉中(例如通過for...in
循環或者Object.keys()
),默認為false
。 - configurable: 如果為
true
,則可以刪除該屬性以及重新定義其描述符,默認為false
。
存取描述符
- get: 一個給屬性提供 getter 方法的函數,如果沒有 getter 則為
undefined
。當訪問該屬性時會調用此函數,默認為undefined
。 - set: 一個給屬性提供 setter 方法的函數,如果沒有 setter 則為
undefined
。當屬性值被修改時會調用此函數,默認為undefined
。
示例:
let person = {};
let age = 20;
Object.defineProperty(person, 'age', {get: function() {console.log('get age');return age;},set: function(value) {if (value < 0) {console.log('年齡不能是負數');} else {age = value;}}
});person.age = 25; // 正常設置年齡
console.log(person.age); // 輸出 25person.age = -5; // 嘗試設置負數年齡
// 輸出 "年齡不能是負數."
console.log(person.age); // 仍然輸出 25
當我們訪問age的時候,可以看到輸出age,設置新的值的時候,如果符合條件,就被修改,不符合條件,就被攔截到,使用自定義的getter
和setter
來重寫了原有的行為,對obj.age進行取值和賦值,這就是數據劫持。
但是上面的代碼有個問題:屬性的值都是局部的
所以我們需要一個全局的變量來保存這個屬性的值.
// value使用了參數默認值
function defineReactive(data, key, value = data[key]) {Object.defineProperty(data, key, {get: function reactiveGetter() {return value},set: function reactiveSetter(newValue) {if (newValue === value) returnvalue = newValue}})
}defineReactive(obj, age, 29)
如果有多個屬性呢,我們要用Observer類來遍歷對象,對每個屬性都進行defineProperty劫持。
class Observer {constructor(value) {this.value = valuethis.walk()}walk() {Object.keys(this.value).forEach((key) => defineReactive(this.value, key))}
}。
如果obj是這種嵌套結構呢?{a:{b:{age:20}}
你可能想到了用遞歸,其實vue也是這么做的。
// 入口函數
function observe(data) {if (typeof data !== 'object') return// 調用Observernew Observer(data)
}class Observer {constructor(value) {this.value = valuethis.walk()}walk() {// 遍歷該對象,并進行數據劫持Object.keys(this.value).forEach((key) => defineReactive(this.value, key))}
}function defineReactive(data, key, value = data[key]) {observe(value)// 如果value是對象,遞歸調用observe來監測該對象// 如果value不是對象,observe函數會直接返回Object.defineProperty(data, key, {get: function reactiveGetter() {return value},set: function reactiveSetter(newValue) {if (newValue === value) returnvalue = newValueobserve(newValue) // 設置的新值也要被監聽}})
}const obj = {a: 1,b: {age: 20}
}observe(obj)
observe、new Observer、defineReactive三者的關系:
執行 observe(obj)
├── 檢查 obj 是否為對象
│ └── true: new Observer(obj),并執行 this.walk() 遍歷 obj 的屬性,執行 defineReactive()
│ ├── defineReactive(obj, 'a')
│ │ ├── 檢查 'a' 的值是否為對象
│ │ │ └── 如果是對象: 遞歸調用 observe(value_of_a)
│ │ └── 使用 Object.defineProperty 對 'a' 進行 getter/setter 劫持
│ ├── defineReactive(obj, 'b')
│ │ ├── 檢查 'b' 的值是否為對象
│ │ │ └── 如果是對象: 遞歸調用 observe(value_of_b)
│ │ └── 使用 Object.defineProperty 對 'b' 進行 getter/setter 劫持
│ └── ...(繼續遍歷 obj 的其他屬性)
└── false: 直接返回
三個函數相互調用從而形成了遞歸。
這一部分只完成了對數據的劫持,有人可能想到,可以在setter中調用渲染函數,那不就可以更新頁面了,也可以這樣做,但是這樣做有個弊端:只要有數據變化,頁面就會重新更新。為了解決這個問題,數據變化時只更新與這個數據有關的DOM結構,怎么才能做到這樣的效果,那就涉及到依賴。
收集依賴
依賴
什么是依賴呢?
假設你想借一本書。你需要向圖書管理員詢問這本書是否可用。如果這本書已經被借出去了,你會等待直到它被歸還。一旦這本書被歸還到圖書館,圖書管理員會通知你這本書現在可以借閱了。
在這個例子中
- 讀者相當于Vue組件。它們需要根據數據的變化來決定何時重新渲染自己。
- 圖書管理員相當于Vue的
Watcher
機制。他們監視著數據的變化,并在數據發生變化時采取行動。 - **書籍的狀態(是否可借)**相當于Vue中的響應式數據。這些數據可以是變量、對象屬性等,當它們發生變化時,依賴于這些數據的組件(讀者)需要得到通知并作出相應的更新。
而Watcher
就是我們說的依賴,Watcher
是一個抽象的類。
每個Watcher
實例訂閱一個或者多個數據,這些數據也被稱為wacther
的依賴;當依賴發生變化,Watcher
實例會接收到數據發生變化這條消息,之后會執行一個回調函數來實現某些功能,比如更新頁面。
[模板解析]↓
[生成渲染函數]↓
[執行渲染函數] → 訪問 data.message↓
[觸發 getter] → message 屬性的 getter 被調用↓
[創建 Watcher] ← 當前正在執行的渲染函數↓
[Dep 收集 Watcher] ← 將當前 Watcher 添加到 message 的依賴列表中↓
[生成虛擬 DOM → 真實 DOM]
實現watcher類
class Watcher {constructor(data, expression, cb) {// data: 數據對象// expression:表達式,如b.c,根據data和expression就可以獲取watcher依賴的數據// cb:依賴變化時觸發的回調this.data = datathis.expression = expressionthis.cb = cb// 初始化watcher實例時訂閱數據this.value = this.get()}get() {const value = parsePath(this.data, this.expression)return value}// 當收到數據變化的消息時執行該方法,從而調用cbupdate() {this.value = parsePath(this.data, this.expression) // 對存儲的數據進行更新cb()}
}function parsePath(obj, expression) {const segments = expression.split('.')for (let key of segments) {if (!obj) returnobj = obj[key]}return obj
}
這里的update方法有點瑕疵,我們可以在定義的回調中訪問this
,并且該回調可以接收到監聽數據的新值和舊值,因此做如下修改
update() {const oldValue = this.valuethis.value = parsePath(this.data, this.expression)this.cb.call(this.data, this.value, oldValue)
}
在源碼中,有targetStack這樣一個變量,也就是我們寫的window.target
我們寫的方式有一個弊端:當我們有兩個嵌套的父子組件,渲染父組件時會新建一個父組件的watcher
,渲染過程中發現還有子組件,就會開始渲染子組件,也會新建一個子組件的watcher
。在我們的實現中,新建父組件watcher
時,window.target
會指向父組件watcher
,之后新建子組件watcher
,window.target
將被子組件watcher
覆蓋,子組件渲染完畢,回到父組件watcher
時,window.target
變成了null
,這就會出現問題,因此,我們用一個棧結構來保存watcher
。
const targetStack = []function pushTarget(_target) {targetStack.push(window.target)window.target = _target
}function popTarget() {window.target = targetStack.pop()
}
Watcher
的get
方法做如下修改
get() {pushTarget(this) // 修改const value = parsePath(this.data, this.expression)popTarget() // 修改return value
}
依賴收集
每個數據都應該維護一個屬于自己的數組,該數組來存放依賴自己的watcher
,我們可以在defineReactive
中定義一個數組dep
,這樣通過閉包,每個屬性就能擁有一個屬于自己的dep
.
function defineReactive(data, key, value = data[key]) {const dep = [] // 存放watcherobserve(value)Object.defineProperty(data, key, {get: function reactiveGetter() {return value},set: function reactiveSetter(newValue) {if (newValue === value) returnvalue = newValueobserve(newValue)dep.notify()}})
}
那么dep是如何收集watcher的呢?
new Watcher()
時執行constructor
,調用了實例的get
方法,實例的get
方法會讀取數據的值,從而觸發了數據的getter
,getter
執行完畢后,實例的get
方法執行完畢,并返回值,constructor
執行完畢,實例化完畢。
所以我們只需要對getter
進行一些修改:
get: function reactiveGetter() {dep.push(watcher) // 新增return value
}
watcher
這個變量從哪里來呢?我們是在模板編譯函數中的實例化watcher
的,getter
中取不到這個實例。為了解決這個問題,需要把watcher
放到全局中,比如說window對象中。
其實可以把dep
抽象成一個類(有點像發布訂閱模式)
Dep類
class Dep {constructor() {this.subs = []}depend() {this.addSub(Dep.target)}notify() {const subs = [...this.subs]subs.forEach((s) => s.update())}addSub(sub) {this.subs.push(sub)}
}
defineReactive
函數只需做相應的修改
function defineReactive(data, key, value = data[key]) {const dep = new Dep() // 修改observe(value)Object.defineProperty(data, key, {get: function reactiveGetter() {dep.depend() // 修改return value},set: function reactiveSetter(newValue) {if (newValue === value) returnvalue = newValueobserve(newValue)dep.notify() // 修改}})
}
在watcher中的代碼里
get() {window.target = thisconst value = parsePath(this.data, this.expression)return value
}
大家可能注意到了,我們沒有重置window.target
。有些同學可能認為這沒什么問題,但是考慮如下場景:有一個對象obj: { a: 1, b: 2 }
我們先實例化了一個watcher1
,watcher1
依賴obj.a
,那么window.target
就是watcher1
。之后我們訪問了obj.b
,會發生什么呢?訪問obj.b
會觸發obj.b
的getter
,getter
會調用dep.depend()
,那么obj.b
的dep
就會收集window.target
, 也就是watcher1
,這就導致watcher1
依賴了obj.b
,但事實并非如此。為解決這個問題,我們做如下修改:
// Watcher的get方法
get() {window.target = thisconst value = parsePath(this.data, this.expression)window.target = null // 新增,求值完畢后重置window.targetreturn value
}// Dep的depend方法
depend() {if (Dep.target) { // 新增this.addSub(Dep.target)}
}
為什么不能寫成window.target = new Watcher()?
因為執行到getter
的時候,實例化watcher
還沒有完成,所以window.target
還是undefined
依賴收集過程:渲染頁面時碰到插值表達式,
v-bind
等需要數據等地方,會實例化一個watcher
,實例化watcher
就會對依賴的數據求值,從而觸發getter
,數據的getter
函數就會添加依賴自己的watcher
,從而完成依賴收集。我們可以理解為watcher
在收集依賴,而代碼的實現方式是在數據中存儲依賴自己的watcher
。
vue2
的做法是每個組件對應一個watcher
,實例化watcher
時傳入的也不再是一個expression
,而是渲染函數,渲染函數由組件的模板轉化而來,這樣一個組件的watcher
就能收集到自己的所有依賴,以組件為單位進行更新,是一種中等粒度的方式。要實現vue2
的響應式系統涉及到很多其他的東西,比如組件化,虛擬DOM
等。
派發更新
實現依賴收集后,我們最后要實現的功能是派發更新,也就是依賴變化時觸發watcher
的回調。
set: function reactiveSetter(newValue) {if (newValue === value) returnvalue = newValueobserve(newValue)dep.forEach(d => d.update()) // 新增 update方法見Watcher類
}
依賴收集圖示:
[執行 this.message = "Hello World"]↓
[觸發 setter]↓
[通知 Dep] → 所有訂閱了 message 的 Watcher 都會被通知↓
[Watcher.update()] → 標記為臟,準備重新渲染↓
[異步更新隊列] → Vue 使用 nextTick 批量更新視圖↓
[重新執行渲染函數] → 生成新的虛擬 DOM 并對比差異↓
[更新真實 DOM]
總體過程
+------------------+ +------------------+
| 渲染函數/組件 | | Watcher 對象 |
| 使用 message |<----->| 記錄哪些組件在 |
+--------+---------+ | 使用該數據 || +--------+---------+| || |v v
+--------+---------------------------+---------+
| Dep(依賴收集器) |
| 每個響應式屬性都有一個 Dep,用來保存 Watcher |
+--------+--------------------------------------+ || +---------------------+| | |v v v[message: 'Hello Vue!'] [其他響應式屬性]|| setter/getter↓數據變化 → 通知 Dep → Dep 通知 Watcher → 更新組件
總體代碼
// 調用該方法來檢測數據
function observe(data) {if (typeof data !== 'object') returnnew Observer(data)
}class Observer {constructor(value) {this.value = valuethis.walk()}walk() {Object.keys(this.value).forEach((key) => defineReactive(this.value, key))}
}// 數據攔截
function defineReactive(data, key, value = data[key]) {const dep = new Dep()observe(value)Object.defineProperty(data, key, {get: function reactiveGetter() {dep.depend()return value},set: function reactiveSetter(newValue) {if (newValue === value) returnvalue = newValueobserve(newValue)dep.notify()}})
}// 依賴
class Dep {constructor() {this.subs = []}depend() {if (Dep.target) {this.addSub(Dep.target)}}notify() {const subs = [...this.subs]subs.forEach((s) => s.update())}addSub(sub) {this.subs.push(sub)}
}Dep.target = nullconst TargetStack = []function pushTarget(_target) {TargetStack.push(Dep.target)Dep.target = _target
}function popTarget() {Dep.target = TargetStack.pop()
}// watcher
class Watcher {constructor(data, expression, cb) {this.data = datathis.expression = expressionthis.cb = cbthis.value = this.get()}get() {pushTarget(this)const value = parsePath(this.data, this.expression)popTarget()return value}update() {const oldValue = this.valuethis.value = parsePath(this.data, this.expression)this.cb.call(this.data, this.value, oldValue)}
}// 工具函數
function parsePath(obj, expression) {const segments = expression.split('.')for (let key of segments) {if (!obj) returnobj = obj[key]}return obj
}// for test
let obj = {a: 1,b: {m: {n: 4}}
}observe(obj)let w1 = new Watcher(obj, 'a', (val, oldVal) => {console.log(`obj.a 從 ${oldVal}(oldVal) 變成了 ${val}(newVal)`)
})
數組處理
要對數組處理的原因
在 Vue 2 的響應式系統中,數組方法需要被重寫的原因主要與 Vue 的響應式機制以及 JavaScript 數組的特性有關。Vue 2 使用 Object.defineProperty
來實現數據的響應式轉換,但是這種方法對數組的某些操作不起作用。
- 無法檢測數組變化:使用
Object.defineProperty
可以很好地追蹤對象屬性的變化(通過 getter 和 setter),但是對于數組,直接修改數組元素(例如arr[0] = newValue
或者arr.length = newLength
)不會觸發 setter,因此 Vue 不能檢測到這些變化并更新視圖。 - 數組方法的直接調用問題:雖然 Vue 不能檢測到上述的數組變化,但它可以攔截對數組原型方法的調用(如
push
,pop
,shift
,unshift
,splice
,sort
,reverse
)。這是因為這些方法會改變原始數組的內容。如果不對這些方法進行重寫,當用戶調用它們時,Vue 將無法知道數組發生了變化,從而導致視圖不更新
為了確保數組的變化能夠被 Vue 檢測到,并且相應地更新視圖,Vue 2 對數組的以下幾種方法進行了重寫:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
這些方法被重寫后,在執行時不僅會對數組本身做出相應的變更,還會觸發視圖更新。這通常通過在原生方法的基礎上包裹一層來實現,即在調用原生方法之前或之后,手動通知依賴該數組的所有 watcher 進行更新。
先對Observer進行修改
class Observer {constructor(value) {this.value = valueif (Array.isArray(value)) {// 代理原型...this.observeArray(value)} else {this.walk(value)}}walk(obj) {Object.keys(obj).forEach((key) => defineReactive(obj, key, obj[key]))}// 需要繼續監聽數組內的元素(如果數組元素是對象的話)observeArray(arr) {arr.forEach((i) => observe(i))}
}
對原型進行代理
在數組實例和Array.prototype
之間增加了一層代理來實現派發更新),數組調用代理原型的方法來派發更新,代理原型再調用真實原型的方法實現原有的功能:
// Observer.js
if (Array.isArray(value)) {Object.setPrototypeOf(value, proxyPrototype) // value.__proto__ === proxyPrototypethis.observeArray(value)
}// array.js
const arrayPrototype = Array.prototype // 緩存真實原型// 需要處理的方法
const reactiveMethods = ['push','pop','unshift','shift','splice','reverse','sort'
]// 增加代理原型 proxyPrototype.__proto__ === arrayProrotype
const proxyPrototype = Object.create(arrayPrototype)// 定義響應式方法
reactiveMethods.forEach((method) => {const originalMethod = arrayPrototype[method]// 在代理原型上定義變異響應式方法Object.defineProperty(proxyPrototype, method, {value: function reactiveMethod(...args) {const result = originalMethod.apply(this, args) // 執行默認原型的方法// ...派發更新...return result},enumerable: false,writable: true,configurable: true})
})
如何派發更新呢,對象是調用dep.nofity來派發更新,由于形成了閉包,每個屬性都有自己的dep。但是如果我們在array.js中定義一個dep,所有數組都會共享,為了解決這個問題,vue
在每個對象身上添加了一個自定義屬性:__ob__
,這個屬性保存自己的Observer
實例,然后再Observer
上添加一個屬性dep
。
對observe
做一個修改:
// observe.js
function observe(value) {if (typeof value !== 'object') returnlet ob// __ob__還可以用來標識當前對象是否被監聽過if (value.__ob__ && value.__ob__ instanceof Observer) {ob = value.__ob__} else {ob = new Observer(value)}return ob
}
Observer
做修改:
constructor(value) {this.value = valuethis.dep = new Dep()// 在每個對象身上定義一個__ob__屬性,指向每個對象的Observer實例def(value, '__ob__', this)if (Array.isArray(value)) {Object.setPrototypeOf(value, proxyPrototype)this.observeArray(value)} else {this.walk(value)}
}// 工具函數def,就是對Object.defineProperty的封裝
function def(obj, key, value, enumerable = false) {Object.defineProperty(obj, key, {value,enumerable,writable: true,configurable: true})
}
//obj: { arr: [...] }變成了obj: { arr: [..., __ob__: {} ], __ob__: {} }這種形式
// array.js
reactiveMethods.forEach((method) => {const originalMethod = arrayPrototype[method]Object.defineProperty(proxyPrototype, method, {value: function reactiveMethod(...args) {const result = originalMethod.apply(this, args)const ob = this.__ob__ // 新增ob.dep.notify() // 新增return result},enumerable: false,writable: true,configurable: true})
})
push, unshift, splice
可能會向數組中增加元素,這些增加的元素也應該被監聽:
Object.defineProperty(proxyPrototype, method, {value: function reactiveMethod(...args) {const result = originalMethod.apply(this, args)const ob = this.__ob__// 對push,unshift,splice的特殊處理let inserted = nullswitch (method) {case 'push':case 'unshift':inserted = argsbreakcase 'splice':// splice方法的第三個及以后的參數是新增的元素inserted = args.slice(2)}// 如果有新增元素,繼續對齊進行監聽if (inserted) ob.observeArray(inserted)ob.dep.notify()return result},enumerable: false,writable: true,configurable: true
})
在對象身上新增一個__ob__
屬性,完成了數組的派發更新,接下來是依賴收集。
依賴收集
執行observe(obj)
后,obj
變成了下面的樣子
obj: {arr: [{a: 1,__ob__: {...} // 增加},__ob__: {...} // 增加],__ob__: {...} // 增加
}
在defineReactive
函數中,為了遞歸地為數據設置響應式,調用了observe(val)
,而現在的observe()
會返回ob
,也就是value.__ob__
,那接收一下這個返回值
// defineReactive.js
let childOb = observe(val) // 修改set: function reactiveSetter(newVal) {if (val === newVal) {return}val = newValchildOb = observe(newVal) // 修改dep.notify()
}
childOb
是什么?
childOb
就是obj.prop.__ob__
,閉包中的dep
與childOb.dep
保存的內容相同。
也就是說,每個屬性(比如arr
屬性)的getter
和setter
不僅通過閉包保存了屬于自己的dep
,而且通過__ob__
保存了自己的Observer
實例,Observer
實例上又有一個dep
屬性。
但是dep
和childOb.dep
保存的watcher
并不完全相同,看obj[arr][0].a
,由于這是一個基本類型,對它調用observe
會直接返回,因此所以沒有__ob__
屬性,但是這個屬性閉包中的dep
能夠收集到依賴自己的watcher
。
所以對get觸發依賴時進行修改:
get: function reactiveGetter() {if (Dep.target) {dep.depend()if (childOb) {childOb.dep.depend() // 新增 }}return val
}
Vue
認為,只要依賴了數組,就等價于依賴了數組中的所有元素,因此,我們需要進一步處理
// defineReactive.js
get: function reactiveGetter() {if (Dep.target) {dep.depend()if (childOb) {childOb.dep.depend()// 新增if (Array.isArray(val)) {dependArray(val)}}}return val
}function dependArray(array) {for (let e of array) {e && e.__ob__ && e.__ob__.dep.depend()if (Array.isArray(e)) {dependArray(e)}}
}
當依賴是數組時,遍歷這個數組,為每個元素的__ob__.dep
中添加watcher
。
渲染watcher
渲染watcher
不需要回調函數,渲染watcher
接收一個渲染函數而不是依賴的表達式,當依賴發生變化時,自動執行渲染函數
new Watcher(app, renderFn)
如何做到自動渲染呢,需要對原來的Watcher
的構造函數做一些改造
constructor(data, expOrFn, cb) {this.data = data// 如果是函數的話if (typeof expOrFn === 'function') {this.getter = expOrFn} else {this.getter = parsePath(expOrFn)}this.cb = cbthis.value = this.get()
}// parsePath的改造,返回一個函數
function parsePath(path) {const segments = path.split('.')return function (obj) {for (let key of segments) {if (!obj) returnobj = obj[key]}return obj}
}
get
修改
get() {pushTarget(this)const data = this.dataconst value = this.getter.call(data, data) // 修改popTarget()return value
}
依賴變化時重新執行渲染函數,需要在派發更新階段做一個更新,修改update方法
update() {// 重新執行get方法const value = this.get()// 渲染watcher的value是undefined,因為渲染函數沒有返回值// 因此value和this.value都是undefined,不會進入if// 如果依賴是對象,要觸發更新if (value !== this.value || isObject(value)) {const oldValue = this.valuethis.value = valuethis.cb.call(this.vm, value, oldValue)}
}function isObject(target) {return typeof target === 'object' && target !== null
}
重復收集
對于相同的屬性,可能會重復收集,為了避免這種情況發生,vue采用了以下方式
為每個dep
添加一個id
let uid = 0constructor() {this.subs = []this.id = uid++ // 增加
}
watcher修改的地方比較多,首先為增加四個屬性
deps, depIds, newDeps, newDepIds
this.deps = [] // 存放上次求值時存儲自己的dep
this.depIds = new Set() // 存放上次求值時存儲自己的dep的id
this.newDeps = [] // 存放本次求值時存儲自己的dep
this.newDepIds = new Set() // 存放本次求值時存儲自己的dep的id
當需要收集watcher
時,由watcher
來決定自己是否需要被dep
收集
// dep.depend
depend() {if (Dep.target) {Dep.target.addDep(this) // 讓watcher來決定自己是否被dep收集}
}// watcher.addDep
addDep(dep) {const id = dep.id// 如果本次求值過程中,自己沒有被dep收集過則進入ifif (!this.newDepIds.has(id)) {// watcher中記錄收集自己的dpthis.newDepIds.add(id)this.newDeps.push(dep)if (!this.depIds.has(id)) {dep.addSub(this)}}
}
newDeps
和newDepIds
用來再一次取值過程中避免重復依賴,比如:{{ name }} -- {{ name }}
deps
和depIds
用來再重新渲染的取值過程中避免重復依賴
再執行get
方法最后會清空newDeps,newDepIds
cleanUpDeps() {// 交換depIds和newDepIdslet tmp = this.depIdsthis.depIds = this.newDepIdsthis.newDepIds = tmp// 清空newDepIdsthis.newDepIds.clear()// 交換deps和newDepstmp = this.depsthis.deps = this.newDepsthis.newDeps = tmp// 清空newDepsthis.newDeps.length = 0}
重新收集依賴
分為兩種,一是刪除無效依賴,二是收集新的依賴,收集新的依賴前面代碼已經展示,但是能夠收集到依賴的基本前提是Dep.target
存在,從Watcher
的代碼中可以看出,只有在get
方法執行過程中,Dep.target
是存在的,因此,我們在update
方法中使用了get
方法來重新觸發渲染函數,而不是getter.call()
。
//刪除無效依賴
cleanUpDeps() {// 增加let i = this.deps.lengthwhile (i--) {const dep = this.deps[i]if (!this.newDepIds.has(dep.id)) {dep.removeSub(this)}}let tmp = this.depIds// ...
}
//在dep中刪除
// Dep.js
removeSub(sub) {remove(this.subs, sub)
}function remove(arr, item) {if (!arr.length) returnconst index = arr.indexOf(item)if (index > -1) {return arr.splice(index, 1)}
}
vue3中的響應式
vue3中使用proxy來進行響應式處理。Object.defineProperty 和 Proxy 有什么區別呢?
Object.defineProperty的缺陷
Object.defineProperty 主要用于修改對象屬性,它通過屬性描述符來實現屬性級別的操作,如數據劫持和屬性變化監聽。但由于屬性描述符的選項有限,其功能也相對有限。該API兼容性良好,因此在早期被廣泛使用。
Vue2 選擇 Object.defineProperty 作為響應式系統的基礎,主要看中其良好的兼容性。通過遞歸定義 getter/setter 和重寫數組方法來實現響應式。但受限于API設計,存在以下缺陷:
- 遞歸調用導致性能損耗
- 無法檢測:
- 屬性的新增/刪除
- 數組索引的直接修改
- 數組長度的直接修改
Vue2 通過 Vue.set 和 Vue.delete 方法解決了前兩個問題,但數組長度修改的問題始終無法解決。
使用Proxy之后
相比之下,Proxy 專門用于對象代理,提供對象級別的攔截。它可以攔截幾乎所有對象操作,包括:
- 屬性訪問/修改/刪除
- 枚舉操作
- in 運算符
- 函數調用
- 原型操作
- new 操作符調用
Proxy 的工作機制是對原對象創建代理,只有通過代理對象的操作才會被攔截。所有對代理對象的修改最終都會作用于原對象,Proxy 僅作為操作攔截和處理的中介層。
可以說 Proxy 完美彌補了 Object.defineProperty 的缺點,Vue3 使用 Proxy 后不再需要遞歸操作、不再需要重寫數組的那七個方法、不再需要 Vue.set 和 Vue.delete。