[TOC]
> [Reactivity Source Code](https://github.com/Code-Pop/vue-3-reactivity)
# Vue 3 響應式
我們將了解新的 Vue 3 響應式系統(reactivity system)。了解它是如何從頭構建的,將幫助您理解 Vue 中使用的設計模式,提高您的 Vue 調試技能,使您能夠使用新的 Vue 3 模塊化響應式庫,甚至自己編寫 Vue 3的源代碼。
在本節課中,我們將使用與 Vue 3 源代碼中相同的技術開始構建一個簡單的響應式系統。
# 理解響應式系統
先以這個簡單的應用程序為例
現看一下下面簡單的的程序:
```
<div id="app">
<div>Price: ${{ product.price }}</div>
<div>Total: ${{ product.price * product.quantity }}</div>
<div>Taxes: ${{ totalPriceWithTax }}</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
product: {
price: 5.00,
quantity: 2
}
},
computed: {
totalPriceWithTax() {
return this.product.price * this.product.quantity * 1.03
}
}
})
</script>
```
Vue 的響應式系統以某種方式知道,如果`price`發生變化,它應該做三件事:
* 在更新網頁上`price`值。
* 重新計算`price * quantity`的表達式,然后更新頁面。
* 再次調用`totalPriceWithTax`函數并更新頁面。
所以,Vue的響應式系統是如何知道`price`變化時要更新什么,以及如何跟蹤所有情況?
這不是 JavaScript 編程的工作方式。 例如,如果我運行以下代碼:
```js
let product = { price: 5, quantity: 2 }
let total = product.price * product.quantity // 10 right?
product.price = 20
console.log(`total is ${total}`)
```
會輸出什么呢?如果我們不借助 Vue,會輸出:
```
>> total is 10
```
Vue 中,我們希望只要`price`或`quantity`得到更新,`total`就會得到更新。 我們想要的結果:
```
>> total is 40
```
可惜 JavaScript 是過程式的,不是響應式的,因此在實際上會不起作用。 為了使`total`是響應式的,我們必須使用JavaScript 使其行為有所不同。
在后面的其余部分中,我們將使用與 Vue 3 相同的方法(與 Vue 2 截然不同)從頭開始構建 Reactive System。 然后,我們將研究 Vue 3源代碼,來查找閱讀我們從頭開始編寫的這些模式。
# 保存代碼以便以后運行
## 問題
如您在上面的代碼中所看到的,為了建立響應式,需要保存計算`total`的方式,以便可以在`price`或`quantity`發生變化時重新運行它。
## 解決方案
首先,我們需要以某種方式告訴我們的應用程序:“存儲我將要運行的影響(effect),可能需要您在其他時間運行它。” 然后,我們要運行代碼,如果`price`或`quantity`變量得到更新,需要再次運行存儲的代碼。

我們可以通過記錄影響(effect)來實現這一點,這樣我們就可以再次運行它。
```
let product = { price: 5, quantity: 2 }
let total = 0
let effect = function () {
total = product.price * product.quantity
})
track() // Remember this in case we want to run it later
effect() // Also go ahead and run it
```
請注意,我們將匿名函數存儲在`effect`變量內,然后調用`track`函數。 使用 ES6 箭頭語法,我也可以這樣寫:
```
let effect = () => { total = product.price * product.quantity }
```
為了定義`track`,我們需要一個存放副作用(effects)的地方,我們可能有很多。我們將創建一個名為`dep`的變量。 之所以稱為依賴,是因為通常在觀察者設計模式下,依賴具有訂閱者(在我們的情況下為effects),這些訂閱者將在對象更改狀態時得到通知。 就像我們在 Vue 2 版本中所做的那樣,我們可以使依賴項成為具有訂閱者數組的類。 但是,由于它需要存儲的只是一組效果,因此我們可以簡單地創建一個 **Set**。
```
let dep = new Set() // Our object tracking a list of effects
```
然后我們的`track`函數可以簡單地將我們的副作用(effects)添加到這個集合中。
```
function track () {
dep.add(effect) // Store the current effect
}
```
如果您不熟悉 JavaScript 數組和 Set之間的區別,則是 Set 不能有重復的值,并且不使用數組之類的索引。 如果您不熟悉,請在[此處](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set)詳細了解 。
我們要存儲 effect(在我們的示例中是`{ total = price * quantity }`),以便可以稍后運行它。 這是dep 的 Set 的可視化展示:

讓我們編寫一個觸發函數來運行我們記錄的所有內容。
```
function trigger() {
dep.forEach(effect => effect())
}
```
這將遍歷我們存儲在`dep`集中的所有匿名函數,并執行每個匿名函數。 然后在我們的代碼中,我們可以:
```
product.price = 20
console.log(total) // => 10
trigger()
console.log(total) // => 40
```
很簡單,對不對? 這里是完整的代碼:
```
let product = { price: 5, quantity: 2 }
let total = 0
let dep = new Set()
function track() {
dep.add(effect)
}
function trigger() {
dep.forEach(effect => effect())
}
let effect = () => {
total = product.price * product.quantity
}
track()
effect()
product.price = 20
console.log(total) // => 10
trigger()
console.log(total) // => 40
```

## 問題:多個屬性
我們可以根據需要繼續跟蹤 effects,但是我們的響應式對象將具有不同的屬性,而且每個屬性都需要它們自己的dep(一組 effects)。看看下面的對象:
```
let product = { price: 5, quantity: 2 }
```
我們的`price`屬性需要自己的 dep(影響集),而我們的`quantity`需要自己的 dep(影響集)。 讓我們再想辦法以來正確記錄這些內容。
## 解決方案:depsMap
現在,當我們調用跟蹤或觸發器時,我們需要知道我們要定位的對象是哪個屬性(`price`或`quantity`)。 為此,我們將創建一個`depsMap`,它是 **Map** 類型(請考慮鍵和值)。 可以將其可視化展示:

請注意,`depsMap`有一個鍵,這個鍵將是我們想要添加(或跟蹤)新 effect 的屬性名。所以我們需要將這個鍵值發送給`track`函數。
```
const depsMap = new Map()
function track(key) {
// Make sure this effect is being tracked.
let dep = depsMap.get(key) // Get the current dep (effects) that need to be run when this key (property) is set
if (!dep) {
// There is no dep (effects) on this key yet
depsMap.set(key, (dep = new Set())) // Create a new Set
}
dep.add(effect) // Add effect to dep
}
}
function trigger(key) {
let dep = depsMap.get(key) // Get the dep (effects) associated with this key
if (dep) { // If they exist
dep.forEach(effect => {
// run them all
effect()
})
}
}
let product = { price: 5, quantity: 2 }
let total = 0
let effect = () => {
total = product.price * product.quantity
}
track('quantity')
effect()
console.log(total) // --> 10
product.quantity = 3
trigger('quantity')
console.log(total) // --> 40
```
## 問題:多個響應對象
現在,我們需要一種為每個對象(例如`product`)存儲`depsMap`的方法。 我們需要另一個`Map`,每個對象一個`Map`,但是關鍵是什么? 如果我們使用 **WeakMap*`,則可以將對象本身用作鍵。[WeakMap](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) 是一個JavaScript Map,僅使用對象作為鍵。 例如:
```
let product = { price: 5, quantity: 2 }
const targetMap = new WeakMap()
targetMap.set(product, "example code to test")
console.log(targetMap.get(product)) // ---> "example code to test"
```
顯然,這不是我們要使用的代碼,但我想向您展示我們的`targetMap`如何將我們的`product`對象用作鍵。 之所以將其稱為`targetMap`,是因為我們將考慮將目標對象定位為目標。 還有一個原因,在下一課中會講得更清楚。 這是我們可視化展示:

當我們調用`track`或`trigger`,我們現在需要知道要定位的對象。因此,我們將在調用目標時同時發送`target`和鍵(the key)。
```js
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()
})
}
}
let product = { price: 5, quantity: 2 }
let total = 0
let effect = () => {
total = product.price * product.quantity
}
track(product, 'quantity')
effect()
console.log(total) // --> 10
product.quantity = 3
trigger(product, 'quantity')
console.log(total) // --> 15
```
現在我們有了一種非常有效的方法來跟蹤對多個對象的依賴關系,這是構建響應式系統時的一大難題。戰斗已經結束了一半。 在下面,我們將發現如何使用 ES6 proxy 自動調用`track`和`trigger`。
# Proxy 與 Reflect
我們以及學習了 Vue 3 如何跟蹤 effects 以在需要時重新運行它們。 但是,我們仍然必須手動調用`track`和`trigger`。 在本節中,我們將學習如何使用 **Reflect** 和 **Proxy** 來自動調用它們。
## 解決方案:掛鉤獲取和設置
我們需要一種方法來掛鉤(或監聽)響應式對象上的 get 和 set 方法。
**GET 屬性 => 我們需要跟蹤當前 effect**
**SET 屬性 => 我們需要觸發此屬性的所有跟蹤依賴項(effects)**
了解如何執行此操作的第一步是了解在帶有 ES6 Reflect 和 Proxy 的 Vue 3 中,我們如何攔截 GET 和 SET 調用。 在 Vue 2 中,使用 ES5 的`Object.defineProperty`進行了此操作。
## 了解ES6 Reflect
要打印出對象屬性,我可以這樣做:
```
let product = { price: 5, quantity: 2 }
console.log('quantity is ' + product.quantity)
// or
console.log('quantity is ' + product['quantity'])
```
但是,我也可以通過使用`Reflect`獲取對象上的值。`Reflect`允許您獲取對象的屬性。 這只是我上面寫的另一種方式:
```
console.log('quantity is ' + Reflect.get(product, 'quantity'))
```
為什么要使用`reflect`? 因為它具有我們稍后需要的功能,后面會說道。
## 了解 ES6 Proxy
代理是另一個對象的占位符,默認情況下,被代理給了該對象。 因此,如果我運行以下代碼:
```
let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {})
console.log(proxiedProduct.quantity)
```
`proxiedProduct`代理了`product`,該`product`返回 2 作為數量。 注意`Proxy {}`中的第二個參數嗎? 稱為`handler`,可用于定義代理對象上的自定義行為,例如攔截`get`和`set`調用。 這些攔截器方法稱為 traps(捕捉器),這是我們如何在`handler`上設置`get` trap的方法:
```js
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)
```
在控制臺中,我會看到:
```shell
Get was called
Not the value
```
我們已經重寫了屬性值被訪問時`get`的返回值。我們應該返回實際的值,可以這樣做:
```
let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {
get(target, key) { // <--- The target (our object) and key (the property name)
console.log('Get was called with key = ' + key)
return target[key]
}
})
console.log(proxiedProduct.quantity)
```
這里`get`函數具有兩個參數,即target(即我們的對象(`product`))和我們嘗試獲取的`key`(為`quantity`)。 現在我們看到:
```
Get was called with key = quantity*
2
```
這也是我們可以使用 Reflect 并向其添加額外參數的地方。
~~~js
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) // <----
}
})
~~~
請注意,我們的`get`還有一個稱為`receiver`的附加參數,我們將其作為參數發送到`Reflect.get`中。 這樣可以確保當我們的對象從另一個對象繼承了值 / 函數時,將使用正確的`this`值。 這就是為什么我們總是在`Proxy`內部使用`Reflect`的原因,這樣我們可以保留我們自定義的原始行為。
現在,我們添加一個 setter 方法,這里應該沒有什么大的驚喜:
```js
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`看起來和`get`非常相似,除了使用了`Reflect.set`,該函數接收設置`target`(product)的`value`。我們預期的輸出是:
```shell
Set was called with key = quantity and value = 4
Get was called with key = quantity
4
```
還有另一種方法可以封裝此代碼,即您在 Vue 3 源代碼中看到的內容。首先,我們將把代理代碼包裝在一個返回代理的`reactive`函數中,如果您使用過 Vue 3 Composition API,您應該會很熟悉這個函數。然后我們將分別聲明我們的`handler`和 traps,并將它們發送到代理中。
```
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)
}
let product = reactive({ price: 5, quantity: 2 }) // <-- Returns a proxy object
product.quantity = 4
console.log(product.quantity)
```
返回的結果與上面相同,但是現在我們可以輕松地創建多個響應式對象。
## 結合 Proxy + Effect 存儲
如果我們使用創建響應式對象的代碼,請記住:
**GET 屬性 => 我們需要跟蹤當前 effect**
**SET 屬性 => 我們需要觸發此屬性的所有跟蹤依賴項(effects)**
開始想象一下,需要在上面的代碼中調用`track`和`trigger`的地方:
```js
function reactive(target) {
const handler = {
get(target, key, receiver) {
let result = Reflect.get(target, key, receiver)
// Track
return result
},
set(target, key, value, receiver) {
let oldValue = target[key]
let result = Reflect.set(target, key, value, receiver)
if (oldValue != result) { // Only if the value changes
// Trigger
}
return result
}
}
return new Proxy(target, handler)
}
```
現在,將這兩段代碼放在一起:
```js
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(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(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)
```
請注意,我們不再需要調用`trigger`和`track`,因為在我們的`get`和`set`方法中已經正確調用了它們。 運行這段代碼可以給我們:
```
before updated quantity total = 10
after updated quantity total = 15
```
哇,我們已經走了很長一段路! 在此代碼穩定之前,只有一個錯誤要修復。 具體來說,我們只希望在會被`effect`函數影響到的響應式對象上調用`track`。 而現在,只要獲得響應式對象屬性,就會調用`track`。
# activeEffect & ref
我們將通過修復一個小錯誤然后實現響應式引用來繼續構建我們的響應式代碼,就像您在 Vue 3 中看到的那樣。前面代碼的底部如下所示:
```js
...
let product = reactive({ price: 5, quantity: 2 })
let total = 0
let effect = () => {
total = product.price * product.quantity
}
effect()
console.log(total)
product.quantity = 3
console.log(total)
```
當我們添加從響應式對象(reactive object)獲取屬性的代碼時,問題就來了,就像這樣:
```
console.log('Updated quantity to = ' + product.quantity)
```
這里的問題是即使我們不在`effect`內,也將調用`track`及其所有功能。 我們只希望在活動的影響(the active effect) 中調用`get`,查找并記錄 effect。
## 解決方案:activeEffect
為了解決這個問題,先創建一個`activeEffect`,這是一個全局變量,用于存儲當前正在運行的 effect 。然后在一個名為`effect`的新函數中進行設置:
```js
let activeEffect = null // The active effect running
...
function effect(eff) {
activeEffect = eff // Set this as the activeEffect
activeEffect() // Run it
activeEffect = null // Unset it
}
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}`
)
```
這里不再需要手動調用`effect`。 它會在我們的新 effect 函數中自動調用。 而且還添加了第二個 effect。 我還更新了`console.log`,使其看起來更像是測試,因此我們可以驗證正確的輸出。 您可以從 [github](https://github.com/Code-Pop/vue-3-reactivity) 上獲取所有代碼。
目前一切都很好,但是我們還需要在`track`函數中進行另一項更改。 它需要使用我們新的`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
}
}
```
現在來運行代碼,如下:
```shell
Before updated total (should be 10) = 10 salePrice (should be 4.5) = 4.5
After updated total (should be 15) = 15 salePrice (should be 4.5) = 4.5
After updated total (should be 30) = 30 salePrice (should be 9) = 9
```
## Ref 的必需需求
我意識到,如果使用`salePrice`而不是`price`,那么我計算`total`的方式可能會更有意義,就像這樣:
```
effect(() => {
total = salePrice * product.quantity
})
```
如果要創建一家真實的商店,則會基于`salePrice`來計算總數。 但是,此代碼不會響應式地起工作。 具體來說,當`product.price`更新時,它將以這種方式響應式地重新計算`salePrice`:
```
effect(() => {
salePrice = product.price * 0.9
})
```
但是,由于`salePrice`不是響應式地,因此不會重新運行技術`total`的 effect。 上面的第一個 effect 不會重新運行。 我們需要某種方法來使`salePrice`具有響應式,如果我們不必將其包裝在另一個響應式對象中,那就太好了。 如果您熟悉Vue 3 Composition API,您可能會認為我應該使用`ref`創建一個 Reactive Reference。 我們開工吧:
```
let product = reactive({ price: 5, quantity: 2 })
let salePrice = ref(0)
let total = 0
```
根據 Vue 文檔,響應式引用接受一個內部值,并返回一個響應的、可變的`ref`對象。`ref `對象具有指向內部值的單個屬性`.value`。 因此,我們需要使用.`value`稍微改變一下 effect。
```
effect(() => {
total = salePrice.value * product.quantity
})
effect(() => {
salePrice.value = product.price * 0.9
})
```
代碼應該可以正常工作,并在更新`salePrice`時正確地更新`total`。但是我們仍然需要定義`ref`。 我們有兩種方法可以做到。
## 1. 用響應式定義引用
首先,我們可以簡單地使用已定義的`reactive`:
```
function ref(intialValue) {
return reactive({ value: initialValue })
}
```
但是,這不是 Vue 3 用基本值(primitives)定義`ref`的方式,因此讓我們以不同的方式實現它。
## 理解 JavaScript 對象訪問器
為了理解 Vue 3 如何定義`ref`,首先需要確保理解對象訪問器。 這些有時也稱為 JavaScript 計算屬性(computed properties)(不要與 Vue 的計算屬性混淆)。 在下面,您可以看到一個使用對象訪問器的簡單示例:
```js
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`和`set` 是 **獲取** `fullName`并相應地 **設置** `fullName`的對象訪問器。 這是純 JavaScript 功能,不是 Vue 的函數。
## 2. 用對象訪問器定義 Ref
使用對象訪問器以及我們的`track`和`trigger`操作,現在可以使用以下方法定義引用:
```
function ref(raw) {
const r = {
get value() {
track(r, 'value')
return raw
},
set value(newVal) {
raw = newVal
trigger(r, 'value')
},
}
return r
}
```
這就是它的全部。現在,當我們運行以下代碼時:
```
...
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}`
)
```
我們得到了預期的結果:
```shell
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`是響應式的,并且`total`會在它發生變化時進行更新!
# 計算值和 Vue 3 源碼
在構建響應式示例時,您可能想知道“為什么在使用`effect`的地方沒有使用`computed`來表示值” :
```
let product = reactive({ price: 5, quantity: 2 })
let salePrice = ref(0)
let total = 0
effect(() => {
salePrice.value = product.price * 0.9
})
effect(() => {
total = salePrice.value * product.quantity
})
```
顯然,如果我要對 Vue 進行編碼,我會將`salePrice`和`total`都寫為計算屬性。 如果您熟悉 Vue 3 composition API,則可能熟悉計算的語法。 們可能會像這樣使用計算后的語法(即使我們尚未定義):
```
let product = reactive({ price: 5, quantity: 2 })
let salePrice = computed(() => {
return product.price * 0.9
})
let total = computed(() => {
return salePrice.value * product.quantity
})
```
有道理吧? 注意`salePrice`計算屬性是如何包含在`total`計算屬性中的,并且使用`.value`進行訪問。 這是我們實現的第一個線索。 看來我們正在創建另一個響應式引用。 下面是創建計算函數的方式:
```
function computed(getter) {
let result = ref() // Create a new reactive reference
effect(() => (result.value = getter())) // Set this value equal to the return value of the getter
return result // return the reactive reference
}
```
您可以在 [Github](https://github.com/Code-Pop/vue-3-reactivity/blob/d497a3fc874c0e856c1315df12994ff0f04b9bb1/07-computed.js) 上完整查看/運行代碼。 我們的代碼打印出來:
```
Before updated quantity total (should be 9) = 9 salePrice (should be 4.5) = 4.5
After updated quantity total (should be 13.5) = 13.5 salePrice (should be 4.5) = 4.5
After updated price total (should be 27) = 27 salePrice (should be 9) = 9
```
## 沒有注意事項的 Vue 響應式
值得一提的是,我們可以對響應式對象進行某些 Vue 2 無法實現的工作。具體地說,我們可以添加新的響應式屬性。 像這樣:
```
...
let product = reactive({ price: 5, quantity: 2 })
...
product.name = 'Shoes'
effect(() => {
console.log(`Product name is now ${product.name}`)
})
product.name = 'Socks'
```
它將輸出:
```
Product name is now Shoes
Product name is now Socks
```
在 Vue 2 中,這是不可能的,因為它使用`Object.defineProperty`將 getter 和 setter 添加到單個對象屬性中實現響應式的。 現在,借助`Proxy`,可以毫無問題地添加新屬性,并且它們可以立即響應的。
## 針對 Vue 3 源代碼測試我們的代碼
您可能想知道,此代碼是否可針對 Vue 3 源工作? 為此,我克隆了 [vue-next](https://github.com/vuejs/vue-next) 庫(當前為alpha 5),運行 `yarn install`,然后`yarn build reactivity`。在`package/reactivity/dist/`中產生來很多文件。 然后,在那里找到`reactivity.cjs.js`文件,把它移到我的示例文件([github 上的示例文件](https://github.com/Code-Pop/vue-3-reactivity)),并測試使用 Vue 的 Reactivity,而編寫代碼:
```js
var { reactive, computed, effect } = require('./reactivity.cjs')
// Exactly the same code here from before, without the definitions
let product = reactive({ price: 5, quantity: 2 })
let salePrice = computed(() => {
return product.price * 0.9
})
let total = computed(() => {
return salePrice.value * product.quantity
})
console.log(
`Before updated quantity total (should be 9) = ${total.value} salePrice (should be 4.5) = ${salePrice.value}`
)
product.quantity = 3
console.log(
`After updated quantity total (should be 13.5) = ${total.value} salePrice (should be 4.5) = ${salePrice.value}`
)
product.price = 10
console.log(
`After updated price total (should be 27) = ${total.value} salePrice (should be 9) = ${salePrice.value}`
)
product.name = 'Shoes'
effect(() => {
console.log(`Product name is now ${product.name}`)
})
product.name = 'Socks'
```
運行 `node 08-vue-reactivity.js`,正如所料,得到了所有相同的結果:
```
Before updated quantity total (should be 9) = 9 salePrice (should be 4.5) = 4.5
After updated quantity total (should be 13.5) = 13.5 salePrice (should be 4.5) = 4.5
After updated price total (should be 27) = 27 salePrice (should be 9) = 9
Product name is now Shoes
Product name is now Socks
```
所以我們的響應式系統和 Vue 一樣好! 好吧,從基本的角度來看……是的,但實際上 Vue 的版本**要復雜得多**。 讓我們看一下組成 Vue 3 的 響應式系統的源文件,以開始熟悉。
## Vue 3 響應式文件
如果在`/packages/reactivity/src/`中查看 Vue 3 源代碼,就會發現以下文件。它們是 TypeScript(ts)文件,但還是能夠閱讀它們(即使您不知道 TypeScript)。
* **effect.ts** - 定義`effect`函數以封裝可能包含響應式引用和對象的代碼(reactive references and objects)。 包含從`get`請求調用的`track`和從`set`請求調用的`trigger`。
* **baseHandlers.ts** - 包含諸如`get`和`set`之類的`Proxy`處理程序,它們調用`track`和`trigger`(來自 effect.ts)。
* **react.ts** - 包含使用`get`和`set`(來自 basehandlers.ts)創建 ES6 代理的響應式語法的功能。
* **ref.ts** - 定義我們如何使用對象訪問器創建響應 **Ref** 引用(就像我們所做的那樣)。 還包含`toRefs`,它將響應式對象轉換為一系列訪問原始代理的響應式引用。
* **compute.ts** - 使用`effect`和對象訪問器定義`computed`函數(與我們所完成的稍有不同)。
還有一些其他文件,但列出的這些文件具有響應式核心功能。 如果您覺得這是一個挑戰,你可能想深入研究[源代碼](https://github.com/vuejs/vue-next)。
# 參考
[Vuemastery - Vue 3 Reactivity](https://coursehunters.online/t/vuemastery-vue-3-reactivity/2960)
- Introduction
- Introduction to Vue
- Vue First App
- DevTools
- Configuring VS Code for Vue Development
- Components
- Single File Components
- Templates
- Styling components using CSS
- Directives
- Events
- Methods vs Watchers vs Computed Properties
- Props
- Slots
- Vue CLI
- 兼容IE
- Vue Router
- Vuex
- 組件設計
- 組件之間的通信
- 預渲染技術
- Vue 中的動畫
- FLIP
- lottie
- Unit test
- Vue3 新特性
- Composition API
- Reactivity
- 使用 typescript
- 知識點
- 附錄
- 問題
- 源碼解析
- 資源