在上一篇【Vue 3 響應(yīng)式原理一 - Vue 3 Reactivity】
中奢人,我們知道了 Vue 3 如何跟蹤effects
,以便在需要時(shí)重新運(yùn)行它們。然而,我們?nèi)匀恍枰謩?dòng)調(diào)用track
和trigger
×构洌現(xiàn)在我們將學(xué)習(xí)如何使用Reflect
和Proxy
來自動(dòng)調(diào)用它們遥昧。
Hooking onto Get and Set
我們需要一種方法來 hook (或偵聽) 我們的響應(yīng)式對(duì)象上的get
和set
。
GET property (訪問屬性) => 調(diào)用track
去保存當(dāng)前 effect
SET property (修改了屬性) => 調(diào)用trigger
來運(yùn)行屬性的 dependencies (effects)
如何做到這些绣檬?在 Vue 3 中我們使用 ES6 的Reflect
和Proxy
去攔截 GET 和 SET 調(diào)用。Vue 2 中是使用 ES5 的Object.defineProperty
實(shí)現(xiàn)這一點(diǎn)的嫂粟。
理解 ES6 Reflect
要打印出一個(gè)對(duì)象的某屬性可以像這樣做:
let product = { price: 5, quantity: 2 }
console.log('quantity is ' + product.quantity)
// or
console.log('quantity is ' + product['quantity'])
然而娇未,也可以使用Reflect
去 GET 對(duì)象上的值。 Reflect
允許你用另一種方式獲取對(duì)象的屬性:
console.log('quantity is ' + Reflect.get(product, 'quantity'))
為什么使用reflect
星虹?因?yàn)樗哂形覀兩院笮枰奶匦粤闾В诶斫?ES6 Proxy 之后再來展示。
理解 ES6 Proxy
Proxy 是另一個(gè)對(duì)象的占位符宽涌,默認(rèn)情況下對(duì)該對(duì)象進(jìn)行委托平夜。 如果我運(yùn)行如下代碼:
let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {})
console.log(proxiedProduct.quantity)
注意到 Proxy 的第二個(gè)參數(shù){}
了嗎?這是一個(gè)handler
卸亮,可用于定義代理對(duì)象(Proxy)上的自定義行為忽妒,例如攔截 get 和 set 調(diào)用。這些攔截器方法稱為traps
(捕捉器),可以幫助我們攔截一些基本操作段直,如屬性查找吃溅、枚舉或函數(shù)調(diào)用。下面是如何在handler
上設(shè)置 get traps
:
let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {
get() {
console.log('Get was called')
return 'Not the value'
}
})
console.log(proxiedProduct.quantity)
我們應(yīng)該返回實(shí)際的值鸯檬,像這樣:
let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {
get(target, key) { // <--- The target (代理的對(duì)象) and key (屬性名)
console.log('Get was called with key = ' + key)
return target[key]
}
})
console.log(proxiedProduct.quantity)
get 函數(shù)有兩個(gè)參數(shù)决侈,target
是我們的對(duì)象(product
)和我們?cè)噲D獲取的key
(屬性),在本例中是quantity
喧务。
當(dāng)我們?cè)?Proxy 中使用Reflect
赖歌,可以添加一個(gè)額外參數(shù),可以被傳遞到Reflect
調(diào)用中功茴。
let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {
get(target, key, receiver) { // <--- notice the receiver
console.log('Get was called with key = ' + key)
return Reflect.get(target, key, receiver) // <----
}
})
這能確保當(dāng)我們的對(duì)象有從其他對(duì)象繼承的值/函數(shù)時(shí)庐冯,this 值能正確地指向調(diào)用對(duì)象。使用 Proxy 的一個(gè)難點(diǎn)就是this
綁定坎穿。我們希望任何方法都綁定到這個(gè) Proxy肄扎,而不是target
對(duì)象。這就是為什么我們總是在Proxy
內(nèi)部使用Reflect
赁酝,這樣我們就能保留我們正在自定義的原始行為。
現(xiàn)在讓我們添加一個(gè)setter
方法:
let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {
get(target, key, receiver) {
console.log('Get was called with key = ' + key)
return Reflect.get(target, key, receiver)
}
set(target, key, value, receiver) {
console.log('Set was called with key = ' + key + ' and value = ' + value)
return Reflect.set(target, key, value, receiver)
}
})
proxiedProduct.quantity = 4
console.log(proxiedProduct.quantity)
set 除了使用Reflect.set
接收值來設(shè)置 target 之外旭等,看起來與 get 非常相似酌呆。輸出也符合我們的預(yù)期。
我們可以通過另一種方式封裝這段代碼搔耕,就像在 Vue 3 源碼中看到的那樣隙袁。首先,我們將這個(gè)代理委托代碼包裝在一個(gè)返回proxy
的響應(yīng)式函數(shù)中弃榨,如果你用過 Vue 3 Composition API菩收,它應(yīng)該看起來很熟悉。然后將包含 get 和 set traps 的handler
常量發(fā)送到我們的proxy
中鲸睛。
function reactive(target) {
const handler = {
get(target, key, receiver) {
console.log('Get was called with key = ' + key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log('Set was called with key = ' + key + ' and value = ' + value)
return Reflect.set(target, key, value, receiver)
}
}
return new Proxy(target, handler) // 創(chuàng)建一個(gè) Proxy 對(duì)象
}
let product = reactive({ price: 5, quantity: 2 }) // <-- Returns a proxy object
product.quantity = 4
console.log(product.quantity)
這會(huì)返回與上面相同的結(jié)果娜饵,但現(xiàn)在我們可以輕松地利用reactive
方法創(chuàng)建多個(gè)響應(yīng)式對(duì)象。
結(jié)合 Proxy + Effect 存儲(chǔ)
回到最初的起點(diǎn):
GET property (訪問屬性) => 我們需要調(diào)用track
去保存當(dāng)前 effect
SET property (修改了屬性) => 我們需要調(diào)用trigger
來運(yùn)行屬性的 dependencies (effects)
track 將檢查當(dāng)前運(yùn)行的是哪個(gè)副作用(effect)官辈,并將其與 target 和 property 記錄在一起箱舞。這就是 Vue 如何知道這個(gè) property 是該副作用的依賴項(xiàng)。
我們可以想象一下上面的reactive
代碼拳亿,需要調(diào)用 track
和 trigger
的地方晴股。
思路整理:
-
當(dāng)一個(gè)值被讀取時(shí)進(jìn)行追蹤:proxy 的
get
處理函數(shù)中track
函數(shù)記錄了該 property 和當(dāng)前副作用。 -
當(dāng)某個(gè)值改變時(shí)進(jìn)行檢測(cè):在 proxy 上調(diào)用
set
處理函數(shù)肺魁。 -
重新運(yùn)行代碼來讀取原始值:
trigger
函數(shù)查找哪些副作用依賴于該 property 并執(zhí)行它們电湘。
直接整上:
const targetMap = new WeakMap() // targetMap stores the effects that each object should re-run when it's updated
function track(target, key) {
// We need to make sure this effect is being tracked.
let depsMap = targetMap.get(target) // Get the current depsMap for this target
if (!depsMap) {
// There is no map.
targetMap.set(target, (depsMap = new Map())) // Create one
}
let dep = depsMap.get(key) // Get the current dependencies (effects) that need to be run when this is set
if (!dep) {
// There is no dependencies (effects)
depsMap.set(key, (dep = new Set())) // Create a new Set
}
dep.add(effect) // Add effect to dependency map
}
function trigger(target, key) {
const depsMap = targetMap.get(target) // Does this object have any properties that have dependencies (effects)
if (!depsMap) {
return
}
let dep = depsMap.get(key) // If there are dependencies (effects) associated with this
if (dep) {
dep.forEach(effect => {
// run them all
effect()
})
}
}
function reactive(target) {
const handler = {
get(target, key, receiver) {
let result = Reflect.get(target, key, receiver)
// Track
track(target, key) // If this reactive property (target) is GET inside then track the effect to rerun on SET
return result
},
set(target, key, value, receiver) {
let oldValue = target[key]
let result = Reflect.set(target, key, value, receiver)
if (oldValue != result) {
// Trigger
trigger(target, key) // If this reactive property (target) has effects to rerun on SET, trigger them.
}
return result
}
}
return new Proxy(target, handler)
}
let product = reactive({ price: 5, quantity: 2 })
let total = 0
let effect = () => {
total = product.price * product.quantity
}
effect()
console.log('before updated quantity total = ' + total)
product.quantity = 3
console.log('after updated quantity total = ' + total)
這段代碼輸出:
before updated quantity total = 10
after updated quantity total = 15
現(xiàn)在我們不再需要調(diào)用trigger
和track
,因?yàn)樗鼈冊(cè)谖覀兊?code>get和set
方法中被合理地調(diào)用。
使用 Proxy 和 Reflect 能帶來什么好處寂呛?
當(dāng)你使用
proxies
時(shí)怎诫,也就是所謂的響應(yīng)式轉(zhuǎn)換,是懶執(zhí)行的昧谊。
而把對(duì)象傳給 Vue 2 的響應(yīng)式時(shí)刽虹,則必須遍歷所有的 key,并且當(dāng)場(chǎng)轉(zhuǎn)換呢诬,以確保它們被訪問時(shí)都是響應(yīng)式的涌哲。
對(duì)于 Vue3,當(dāng)調(diào)用reactive
時(shí)尚镰,返回的是一個(gè)proxy
代理對(duì)象阀圾,并且只會(huì)在需要的時(shí)候才去轉(zhuǎn)換嵌套的對(duì)象。有點(diǎn)像"懶加載"狗唉。這樣做的好處打個(gè)比方初烘,當(dāng)你進(jìn)行分頁渲染,那么只有第一頁需要的10個(gè)object
需要經(jīng)過響應(yīng)式轉(zhuǎn)化分俯。這對(duì)應(yīng)用程序而言可以節(jié)省很多時(shí)間肾筐,特別是當(dāng)程序擁有龐大的列表對(duì)象時(shí)。
我們已經(jīng)前進(jìn)了一大步缸剪!在此代碼穩(wěn)固之前吗铐,只有一個(gè) bug 需要修復(fù)。具體來說杏节,我們只希望track
在 響應(yīng)式對(duì)象有被effect
使用 時(shí)才被調(diào)用』I現(xiàn)在只要響應(yīng)式對(duì)象屬性是get
,就會(huì)調(diào)用track
奋渔。我們將在下一篇中完善這一點(diǎn)镊逝。
Vue 3 響應(yīng)式原理一 - Vue 3 Reactivity
Vue 3 響應(yīng)式原理二 - Proxy and Reflect
Vue 3 響應(yīng)式原理三 - activeEffect & ref
Vue 3 響應(yīng)式原理四 - Computed Values & Vue 3 源碼