Vue 3 會為 data 建立一個 Proxy
物件,並在裏面建立 getter
和 setter
來取值和更新值,藉此實現響應式。因此不用直接操作原本的 data,而是操作代理 data 的 Proxy 的物件。相反, Vue 2 因為使用 Object.defineProperty
來為 data 的最外層每一個屬性加上 getter
和setter
,所以 Vue 2 是直接修改 data。
同時,因為現在是操作 Proxy
,所以即使 data 裏有多層物件資料,我們仍然可以透過 Proxy
裏建立的 setter
來修改多層物件資料。但 Vue 2 就不能,因為 Vue 2 只是為 data 的最外層屬性逐一加上 getter
和 setter
,因此內層的資料並沒有 setter
,無法被正確修改。
以下會詳細解說 Vue 3 如何使用 Proxy 。承接上一篇討論 Vue 實現響應式(reactivity)的原理。這篇集中整理有關 Vue 3 響應式的原理。內容主要是參考 Vue 3 官方教學影片以及文件,並以個人理解和整理來作解說。
Object.defineProperty()
逐一加上 get
和 set
Proxy 是代理的意思。就像在你要操作的資料前放置一個欄截器,每次操作資料時,都會先跑在 Proxy 裏的程式。以下是一個簡單例子:
const target = {
user: {
name: "Alysa",
jobTitle: "front-end developer",
},
};
const p = new Proxy(target, {
get(target, key) {
return target[key];
},
set(target, key, value) {
// key 是 name, value 是 Tom
target[key] = value;
return true;
},
});
p.user.name = "Tom"; // 觸發 set
console.log(p.user.name); // Tom
透過建立一個 Proxy 物件,並指向代理 target
這個物件。之後觸發 setter
來修改 user
裏的 name
。
在討論如何用 Proxy 實現響應式之前,簡單重溫要實現響應式所需要解決的問題。這一部分在上一篇已經提過,在這裏再簡單總結一次。重用上一篇的例子。假設現在有以下情景:
const data = {
price: 100,
quantity: 2,
}
const total = () => data.price * data.quantity
console.log(total()) // 200
當我修改了 price
,照理 total
也要隨之然更新,所以 total
也要被重新執行,以下是錯誤示範:
let total;
const data = {
price: 100,
quantity: 2,
}
total = data.price * data.quantity
console.log(total) // 200
data.price = 200
console.log(total) // 仍然是 200,但我們期望是 400
正確做法是再次計算 total
的公式,才可以更新 total
:
let total;
const data = {
price: 100,
quantity: 2,
}
total = data.price * data.quantity
console.log(total) // 200
data.price = 200
// 再計算一次,才會更新 total
total = data.price * data.quantity
console.log(total) // 400
以上簡單例子,可見所謂的響應式,要達成兩件事才可以完成:
price
值的元件第一點,Vue 3 與 Vue 2 的原理是相同。同樣是在修改值時,就會觸發 set
。並在 set
裏執行更新舊值的動作。但是, Vue 2 是在 Object.definedProperty()
裏的所定義的 set
,而 Vue 3 就是在 Proxy
的 handler
函式裏定義 set
。
const data = {
price: 100,
quantity: 2,
}
const proxyData = new Proxy(data, {
get(target, key, receiver) {
//...(先作省略)執行把依賴儲存起來的程式碼
let result = Reflect.get(target, key, receiver)
return result
},
set(target, key, value, receiver){
// 以下會回傳布林值
let bol = Reflect.set(target, key, value, receiver)
//...(先作省略)執行所有依賴,更新所有受這依賴影響的值
// set handler 規定要回傳 true
return bol
}
})
proxyData.price = 1000 // 觸發 set()
console.log(proxyData.price) // 觸發 get()。結果是 1000
以上就是 Vue 3 使用 Proxy
來實現更新的概念。利用 Proxy
把 data
包起來,並在 handler
裏使用 set
和 get
。所以,每次我們操作 data
時,我們是操作由 Vue 產生出來的 Proxy
物件,而非直接修改本身data
這個物件,而它本身亦沒有響應式(reactive)。擁有響應式的是經過 Vue 包裝出來的Proxy
物件。相反,Vue 2 是直接修改 data
這物件,而且把它變成響應式。
另外,也補充說明Reflect
以及receiver
參數。Reflect
是等同於使用.
或 []
來訪問物件:
Reflect.get(data, 'price')
// 等於
data.price
Reflect.set(data, 'price', 1000)
// 等於
data.price = 1000
receiver
參數並非必需。 只是為了確保當我們所訪問的物件,如果它本身有繼承其他物件時,this
是指向仍然是該物件本身。詳細解說可參考這篇文章提及的 Reflect 和 receiver 例子。因此,Vue 使用了 Reflect
來取值和修改值,而非一般像是obj.a
或是 obj['a']
這樣的寫法。
price
值的元件第二點,因為 price
改變了,所以 total
就要重新計算,換言之所有用到 total
的元件都要重新渲染。但 Vue 怎麼知道哪一個元件有用到 total
這個值?同時又怎樣知道當 price
更新時, total
要隨之然更新?
這些問題,不論是 Vue 2 還是 Vue 3 所用的手法雖然不相同,但是概念是相同。概念一樣是:
total = data.price * data.quantity
收集起來。data
裏的屬性做記錄。記錄它們會牽涉到哪些依賴。所以,當某個 data
裏的屬性的值更新時(即是 price
),就會按這個紀錄,找出此值連帶的所有依賴程式碼,並重新執行這堆依賴(即是 total = ...
),從而更新所有有牽涉到此屬性的值 (即是 total
)。第一點,Vue 2 的做法是把這些依賴都存放在每個 data
屬性裏所建立的 Dep
實體。但 Vue 3 的做法是用 Map
來存放。概念如下圖:
值得一提是 Vue 3 是使用 Set
來存放所有依賴,Set
物件裏只會存放唯一值,裏面不會有重複的值。所以裏面不會有重複的依賴。
第二點,Vue 2 與 3 的原理是一樣。一樣是透過觸發 set
,並在 set
裏面把之前儲存起來的依賴取出來,並重新跑一次。但因為第一點提到,記錄的手法不一樣了,所以實際上程式碼也會不一樣。
Vue 2 是觸發 dep.depend()
來儲存依賴,並用 dep.notify()
來執行依賴並更新值。但 Vue 3 是觸發 track()
來達成前者,trigger()
來達成後者。
接下來看看 trigger()
和 track()
的程式碼及其概念:
// 儲存不同響應式物件,例如是 "data"
const targetMap = new WeakMap()
// 觸發 get 時會跑 track,儲存依賴:
function track(target, key) {
// 取得 depsMap
let depsMap = targetMap.get(target)
if (!depsMap) {
// 如果還沒建立 depsMap,就建立一個
targetMap.set(target, (depsMap = new Map()))
}
// 取得對應 dep 實體
let dep = depsMap.get(key)
if (!dep) {
// 如果沒有 dep 實體,就建立一個
depsMap.set(key, (dep = new Set())) // Create a new Set
}
// 把依賴儲存起來
dep.add(effect)
}
// 觸發 set 時會跑 trigger,執行所有依賴來更新所有值:
function trigger(target, key) {
// 取得 depsMap
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
// 取得對應的 dep 實體
let dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => {
// 跑一次所有依賴
effect()
})
}
}
所以,如果把之前討論到的 Proxy
部分,以及以上的track()
和 trigger()
合併起來,就是 Vue 3 實現響應式的概念。
https://codepen.io/alysachan/pen/VwWLGGJ
Proxy
來為資料實現響應式(reactivity),並在裏面建立 setter
和 getter
。setter
裏解決。第二點,涉及到 Vue 怎麼把依賴儲存起來,以及怎樣重新執行這些依賴。Vue 3 是使用 Map
、Set
,以及 dep
實體來儲存依賴。當值被修改時,就儲透過查找Map
,去找出對應的 dep
實體,把這實體裏的所有依賴跑一次,從而更新所有受影響的值。最後,這篇文章只是簡單示範了 Vue 3 響應式的概念,實際上 Vue 在實行此概念時是更加複雜。
Proxy and Reflect
一起來了解 Javascript 中的 Proxy 與 Reflect