在本篇我們將修復一個小 bug 來繼續(xù)構建我們的響應式代碼,然后實現(xiàn)響應式引用。
繼續(xù)之前的代碼:
...
let product = reactive({ price: 5, quantity: 2 })
let total = 0
let effect = () => {
total = product.price * product.quantity
}
effect() // 活躍 effect
console.log(total)
product.quantity = 3
// 添加了一段獲取響應式對象的屬性的代碼
console.log('Updated quantity to = ' + product.quantity)
console.log(total)
當我們從響應式對象中獲取屬性時,問題就出現(xiàn)了:
在新增的console.log
訪問product.quantity
時骤菠,track
及它里面的所有方法都會被調(diào)用瞄沙,即使這段代碼不在effect
(就是我們常說的副作用)中己沛。我們只想查找并記錄 內(nèi)部調(diào)用了get
property (訪問屬性) 的活躍 effect。
activeEffect
為了解決這個問題距境,我們首先創(chuàng)建一個activeEffect
全局變量申尼,用于存儲當前運行的effect
。然后我們將在一個名為effect
的新函數(shù)中設置它垫桂。
let activeEffect = null // 運行的 active effect
...
function effect(eff) {
activeEffect = eff // 把要運行的匿名函數(shù)賦給 activeEffect
activeEffect() // 運行它
activeEffect = null // 再把 activeEffect 設置為 null
}
let product = reactive({ price: 5, quantity: 2 })
let total = 0
effect(() => {
total = product.price * product.quantity
})
effect(() => {
salePrice = product.price * 0.9
})
console.log(`Before updated total (should be 10) = ${total} salePrice (should be 4.5) = ${salePrice}`)
product.quantity = 3
console.log(`After updated total (should be 15) = ${total} salePrice (should be 4.5) = ${salePrice}`)
product.price = 10
console.log(`After updated total (should be 30) = ${total} salePrice (should be 9) = ${salePrice}`)
現(xiàn)在我們不再需要手動調(diào)用 effect
师幕。它會在我們新的effect
函數(shù)中自動調(diào)用。我們還添加了第二個effect
诬滩,然后用console.log
測試來驗證輸出霹粥。你可以從 GitHub 上獲取并嘗試所有代碼:vue-3-reactivity
到目前為止一切順利,但我們還需要做一項更改疼鸟,那就是在track
函數(shù)中使用我們新的activeEffect
后控。
function track(target, key) {
if (activeEffect) { // <------ Check to see if we have an activeEffect
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set())) // Create a new Set
}
dep.add(activeEffect) // <----- Add activeEffect to dependency map
}
}
現(xiàn)在運行我們的代碼會輸出:
Ref
我們發(fā)現(xiàn)使用salePrice
而不是price
來計算總數(shù)應該更準確,于是把第一個effect
修改如下:
effect(() => {
total = salePrice * product.quantity
})
如果我們正在創(chuàng)建一個真實的 store空镜,我們可能會根據(jù)salePrice
來計算 total
浩淘。然而捌朴,這句代碼不會響應式工作。當product.price
更新時馋袜,它會響應式地重新計算salePrice
男旗,因為有這個副作用:
effect(() => {
salePrice = product.price * 0.9
})
但是由于salePrice
不是響應式的,所以它的變更不會重新計算 total
的影響欣鳖。我們上面的第一個副作用不會重新運行察皇。我們需要一些方法來使salePrice
具有響應性,如果你熟悉 Composition API泽台,你可能認為應該使用ref
來創(chuàng)建一個響應式引用什荣,那就這樣做吧:
let product = reactive({ price: 5, quantity: 2 })
let salePrice = ref(0)
let total = 0
根據(jù) Vue 文檔,響應性引用采用內(nèi)部值并返回一個具有響應性和可維護的ref
對象怀酷。ref
對象有一個指向內(nèi)部值的屬性.value
稻爬。所以我們需要稍微修改一下我們的effect
。
effect(() => {
total = salePrice.value * product.quantity
})
effect(() => {
salePrice.value = product.price * 0.9
})
我們的代碼現(xiàn)在應該起效了蜕依,當salePrice
更新時能正確更新total
桅锄。但是我們?nèi)匀恍枰ㄟ^ref
定義。這個ref
又是怎么實現(xiàn)的呢样眠?我們有兩種方式友瘤。
1. 通過 Reactive 定義 Ref
簡單地通過reactive
包裝
function ref(intialValue) {
return reactive({ value: initialValue })
}
然而,這不是 Vue 3 用真正原始定義 ref 的方式
理解 JavaScript Object Accessors - 對象訪問器
首先需要確保先熟悉對象訪問器(object accessors)檐束,有時也稱為 JavaScript 的 computed 屬性(不要和 Vue 的計算屬性混淆)辫秧。
下面??是 Object Accessors 的一個簡單示例:
let user = {
firstName: 'Gregg',
lastName: 'Pollack',
get fullName() {
return `${this.firstName} ${this.lastName}`
},
set fullName(value) {
[this.firstName, this.lastName] = value.split(' ')
},
}
console.log(`Name is ${user.fullName}`)
user.fullName = 'Adam Jahr'
console.log(`Name is ${user.fullName}`)
get fullName
和 set fullName
這兩個獲取/設置fullName
值的函數(shù)就是對象訪問器。這是純 JavaScript被丧,不是 Vue 的特性盟戏。
2. 通過 Object Accessors 定義 Ref
在對象訪問器內(nèi)配合使用我們的track
和trigger
操作,我們可以這樣定義 ref:
function ref(raw) {
const r = {
get value() {
track(r, 'value')
return raw
},
set value(newVal) {
raw = newVal
trigger(r, 'value')
},
}
return r
}
這就是全部了甥桂。
這樣做是因為:
ref
設計的初衷就是為包裝一個內(nèi)部值而服務柿究,如果用reactive
包裹的方式封裝它,這樣的“ref
”就允許額外添加屬性格嘁,違背了最初的目的笛求。所以ref
不應該被當作一個reactive
對象。另外還有出于性能的考慮糕簿,用對象字面量創(chuàng)建ref
會更節(jié)省性能。
當我們運行下面??的代碼:
function ref(raw) {
const r = {
get value() {
track(r, 'value')
return raw
},
set value(newVal) {
raw = newVal
trigger(r, 'value')
},
}
return r
}
function effect(eff) {
activeEffect = eff
activeEffect()
activeEffect = null
}
let product = reactive({ price: 5, quantity: 2 })
let salePrice = ref(0)
let total = 0
effect(() => {
total = salePrice.value * product.quantity
})
effect(() => {
salePrice.value = product.price * 0.9
})
console.log(
`Before updated quantity total (should be 9) = ${total} salePrice (should be 4.5) = ${salePrice.value}`
)
product.quantity = 3
console.log(
`After updated quantity total (should be 13.5) = ${total} salePrice (should be 4.5) = ${salePrice.value}`
)
product.price = 10
console.log(
`After updated price total (should be 27) = ${total} salePrice (should be 9) = ${salePrice.value}`
)
能夠得到我們所期望的:
Before updated total (should be 10) = 10 salePrice (should be 4.5) = 4.5
After updated total (should be 13.5) = 13.5 salePrice (should be 4.5) = 4.5
After updated total (should be 27) = 27 salePrice (should be 9) = 9
salePrice
現(xiàn)在是響應式的了狡孔,total
在它更新時也同步更新了懂诗。
Vue 3 響應式原理一 - Vue 3 Reactivity
Vue 3 響應式原理二 - Proxy and Reflect
Vue 3 響應式原理三 - activeEffect & ref
Vue 3 響應式原理四 - Computed Values & Vue 3 源碼