vue2源碼解析——響應式原理

文章目錄

  • 引言
  • 數據劫持
  • 收集依賴
  • 數組處理
  • 渲染watcher
  • vue3中的響應式

引言

vue的設計思想是數據雙向綁定、數據與UI自動同步,即數據驅動視圖。

為什么會這樣呢?這就不得不提vue的響應式原理了,在使用vue的過程中,我被vue的響應式設計深深著迷 ,下面我們就從源碼的角度,來分析一下vue是如何實現響應式原理的。

在vue2中,主要分為三個過程:

  1. 數據劫持:Vue 會遍歷組件實例的所有屬性,并使用 Object.defineProperty 將這些屬性轉換為 getter/setter 形式。這樣做的目的是為了追蹤依賴以及觸發更新。
  2. 依賴收集:當渲染函數執行時,如果訪問了響應式數據,那么這個訪問會被記錄下來,形成一個“依賴”。這意味著哪些視圖或計算屬性依賴于當前的數據。
  3. 視圖更新:一旦某個響應式數據發生改變(即調用了 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,設置新的值的時候,如果符合條件,就被修改,不符合條件,就被攔截到,使用自定義的gettersetter來重寫了原有的行為,對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,之后新建子組件watcherwindow.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()
}

Watcherget方法做如下修改

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方法會讀取數據的值,從而觸發了數據的gettergetter執行完畢后,實例的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 }我們先實例化了一個watcher1watcher1依賴obj.a,那么window.target就是watcher1。之后我們訪問了obj.b,會發生什么呢?訪問obj.b會觸發obj.bgettergetter會調用dep.depend(),那么obj.bdep就會收集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 來實現數據的響應式轉換,但是這種方法對數組的某些操作不起作用。

  1. 無法檢測數組變化:使用 Object.defineProperty 可以很好地追蹤對象屬性的變化(通過 getter 和 setter),但是對于數組,直接修改數組元素(例如 arr[0] = newValue 或者 arr.length = newLength)不會觸發 setter,因此 Vue 不能檢測到這些變化并更新視圖。
  2. 數組方法的直接調用問題:雖然 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__,閉包中的depchildOb.dep保存的內容相同。

也就是說,每個屬性(比如arr屬性)的gettersetter不僅通過閉包保存了屬于自己的dep,而且通過__ob__保存了自己的Observer實例,Observer實例上又有一個dep屬性。

但是depchildOb.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)}}
}
  1. newDepsnewDepIds用來再一次取值過程中避免重復依賴,比如:{{ name }} -- {{ name }}
  2. depsdepIds用來再重新渲染的取值過程中避免重復依賴

再執行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設計,存在以下缺陷:

  1. 遞歸調用導致性能損耗
  2. 無法檢測:
    • 屬性的新增/刪除
    • 數組索引的直接修改
    • 數組長度的直接修改
      Vue2 通過 Vue.set 和 Vue.delete 方法解決了前兩個問題,但數組長度修改的問題始終無法解決。
      使用Proxy之后
      相比之下,Proxy 專門用于對象代理,提供對象級別的攔截。它可以攔截幾乎所有對象操作,包括:
  • 屬性訪問/修改/刪除
  • 枚舉操作
  • in 運算符
  • 函數調用
  • 原型操作
  • new 操作符調用

Proxy 的工作機制是對原對象創建代理,只有通過代理對象的操作才會被攔截。所有對代理對象的修改最終都會作用于原對象,Proxy 僅作為操作攔截和處理的中介層。

可以說 Proxy 完美彌補了 Object.defineProperty 的缺點,Vue3 使用 Proxy 后不再需要遞歸操作、不再需要重寫數組的那七個方法、不再需要 Vue.set 和 Vue.delete。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/83129.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/83129.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/83129.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

gcc相關內容

gcc 介紹&#xff1a;linux就是由gcc編譯出來的&#xff0c;而且好像之前Linux只支持gcc編譯。gcc全稱為gnu compiler collection&#xff0c;它是gnu項目的一個組成部分。gnu致力于創建一個完全自由的操作系統&#xff0c;我感覺意思就是完全開源的操作系統。gnu有很多組件和…

android 圖片背景毛玻璃效果實現

圖片背景毛玻璃效果實現 1 依賴 // Glide implementation("com.github.bumptech.glide:glide:4.16.0") kapt("com.github.bumptech.glide:compiler:4.16.0") implementation("jp.wasabeef:glide-transformations:4.3.0") 2 布局<com.googl…

【Java開發日記】你會不會5種牛犇的yml文件讀取方式?

前言 除了爛大街的Value和ConfigurationProperties外&#xff0c;還能夠通過哪些方式&#xff0c;來讀取yml配置文件的內容&#xff1f; 1、Environment 在Spring中有一個類Environment&#xff0c;它可以被認為是當前應用程序正在運行的環境&#xff0c;它繼承了PropertyReso…

Spring Boot事務失效場景及解決方案

事務失效場景1&#xff1a;方法非public修飾 原因 Spring事務基于動態代理&#xff08;AOP&#xff09;實現&#xff0c;非public方法無法被代理攔截&#xff0c;導致事務失效。 代碼示例 Service public class OrderService {Transactionalprivate void createOrder() { //…

電子電路:怎么理解時鐘脈沖上升沿這句話?

時鐘脈沖是數字電路中用于同步各組件操作的周期性信號&#xff0c;通常表現為高低電平交替的方波。理解其關鍵點如下&#xff1a; 時鐘脈沖的本質&#xff1a; 由晶振等元件生成&#xff0c;呈現0/1&#xff08;低/高電平&#xff09;的規律振蕩每個周期包含上升沿→高電平→下…

docker部署redis mysql nacos seata rabbitmq minio onlyoffice nginx實戰

docker部署redis mysql nacos seata rabbitmq minio onlyoffice nginx實戰 一、環境介紹 操作系統&#xff1a;ubuntu22.04 軟件環境&#xff1a;docker、docker-compose 二、docker安裝 版本規定到26.1.3版本過低會引起莫名其妙的問題。打開終端。更新軟件包列表&#x…

全面解析:npm 命令、package.json 結構與 Vite 詳解

全面解析&#xff1a;npm 命令、package.json 結構與 Vite 詳解 一、npm run dev 和 npm run build 命令解析 1. npm run dev 作用&#xff1a;啟動開發服務器&#xff0c;用于本地開發原理&#xff1a; 啟動 Vite 開發服務器提供實時熱更新&#xff08;HMR&#xff09;功能…

【Oracle】TCL語言

個人主頁&#xff1a;Guiat 歸屬專欄&#xff1a;Oracle 文章目錄 1. TCL概述1.1 什么是TCL&#xff1f;1.2 TCL的核心功能 2. 事務基礎概念2.1 事務的ACID特性2.2 事務的生命周期 3. COMMIT語句詳解3.1 COMMIT基礎語法3.2 自動提交與手動提交3.3 提交性能優化 4. ROLLBACK語句…

OpenCV CUDA模塊直方圖計算------用于在 GPU 上執行對比度受限的自適應直方圖均衡類cv::cuda::CLAHE

操作系統&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 編程語言&#xff1a;C11 算法描述 cv::cuda::CLAHE 是 OpenCV 的 CUDA 模塊中提供的一個類&#xff0c;用于在 GPU 上執行對比度受限的自適應直方圖均衡&#xff08;Contrast Limi…

OpenGAN:基于開放數據生成的開放集識別

簡介 簡介&#xff1a;這次學習的OpenGAN主要學習一個思路&#xff0c;跳出傳統GAN對于判斷真假的識別到判斷是已知種類還是未知種類。重點內容不在于代碼而是思路&#xff0c;會簡要給出一個設計的代碼。 論文題目&#xff1a;OpenGAN: Open-Set Recognition via Open Data …

隨機游動算法解決kSAT問題

input&#xff1a;n個變量的k-CNF公式 ouput&#xff1a;該公式的一組滿足賦值或宣布沒有滿足賦值 算法步驟&#xff1a; 隨機均勻地初始化賦值 a ∈ { 0 , 1 } n a\in\{0,1\}^n a∈{0,1}n.重復t次&#xff08;后面會估計這個t&#xff09;&#xff1a; a. 如果在當前賦值下…

企業上線ESOP電子作業指導書系統實現車間無紙化的投入收益數據綜合分析

企業上線ESOP電子作業指導書系統實現車間無紙化的投入收益數據綜合分析 一、成本節約&#xff1a;無紙化直接降低運營成本 紙張與耗材費用銳減 o 杭州科創致遠案例&#xff1a;某汽配企業引入無紙化系統后&#xff0c;年節省紙張耗材費用超50萬元。通過電子化替代傳統紙質文檔…

高并發抽獎系統優化方案

引子 最近接觸了一個抽獎的項目&#xff0c;由于用戶量比較大&#xff0c;而且第三方提供的認證接口并發量有限&#xff0c;為了保證服務的高可用性&#xff0c;所以對高并限制發有一定的要求。經過一系列研究和討論&#xff0c;做出了以下一些優化方案。 需求分析 根據用戶量…

STM32八股【10】-----stm32啟動流程

啟動流程 1.上電復位 2.系統初始化 3.跳轉到 main 函數 啟動入口&#xff1a; cpu被清空&#xff0c;程序從0x00000000開始運行0x00000000存放的是reset_handler的入口地址0x00000000的實際位置會變&#xff0c;根據不同的啟動模式決定啟動模式分為&#xff1a; flash啟動&a…

LLMTIME: 不用微調!如何用大模型玩轉時間序列預測?

今天是端午節&#xff0c;端午安康&#xff01;值此傳統佳節之際&#xff0c;我想和大家分享一篇關于基于大語言模型的時序預測算法——LLMTIME。隨著人工智能技術的飛速發展&#xff0c;利用大型預訓練語言模型&#xff08;LLM&#xff09;進行時間序列預測成為一個新興且極具…

在VirtualBox中打造高效開發環境:CentOS虛擬機安裝與優化指南

&#x1f525;「炎碼工坊」技術彈藥已裝填&#xff01; 點擊關注 → 解鎖工業級干貨【工具實測|項目避坑|源碼燃燒指南】 一、為何選擇VirtualBox CentOS組合&#xff1f; 對于程序員而言&#xff0c;構建隔離的開發測試環境是剛需。VirtualBox憑借其跨平臺支持&#xff08;W…

LeeCode 98. 驗證二叉搜索樹

給你一個二叉樹的根節點 root &#xff0c;判斷其是否是一個有效的二叉搜索樹。 有效 二叉搜索樹定義如下&#xff1a; 節點的左子樹只包含 小于 當前節點的數。節點的右子樹只包含 大于 當前節點的數。所有左子樹和右子樹自身必須也是二叉搜索樹。 提示&#xff1a; 樹中節…

Python簡易音樂播放器開發教程

&#x1f4da; 前言 編程基礎第一期《12-30》–音樂播放器是日常生活中常用的應用程序&#xff0c;使用Python和pygame庫可以輕松實現一個簡易的音樂播放器。本教程將詳細講解如何開發一個具有基本功能的音樂播放器&#xff0c;并解析其中涉及的Python編程知識點。 &#x1f6e…

ssh連接斷開,保持任務后臺執行——tmux

目錄 **核心用途****基礎使用方法**1. **安裝 tmux**2. **啟動新會話**3. **常用快捷鍵&#xff08;需先按 Ctrlb 前綴&#xff09;**4. **會話管理命令**5. **窗格操作進階** **典型工作流****注意事項****配置文件&#xff08;~/.tmux.conf&#xff09;** tmux&#xff08; …

3D Gaussian splatting 04: 代碼閱讀-提取相機位姿和稀疏點云

目錄 3D Gaussian splatting 01: 環境搭建3D Gaussian splatting 02: 快速評估3D Gaussian splatting 03: 用戶數據訓練和結果查看3D Gaussian splatting 04: 代碼閱讀-提取相機位姿和稀疏點云3D Gaussian splatting 05: 代碼閱讀-訓練整體流程3D Gaussian splatting 06: 代碼…