?本篇博文可能學到的知識點
- 更好的理解 Vue 響應式工作原理
- 學習 Vue 的設計模式
- 學習 Proxy API
- 使用 Proxy 實現觀察者模式
現代前端開發必不可少會用到的 Vue、React 等框架,這些框架的共同之處在於都提供了響應式(Reactive)和元件化(Composable)的檢視元件,元件化開發重新定義了前端開發技術棧。結合前端構建工具以及基於框架出現的各種經過精心設計的UI元件庫,讓前端也進入到了一個工程化的時代。構建頁面變得從未有過的簡潔高效。
如果你是一名經驗豐富(nian ling da)的程式設計師,或多或少都會接觸到沒有這些框架之前的狀態,那時候我們還手持 jQuery 利器,操縱著一手好 dom,當你初次接觸到響應式框架的時候或許會被它的好用所驚豔到。我們只需要改變資料,dom就更新了。本篇博文主要是來討論被驚豔到的響應式框架是如何實現的。我們首先來看看 Vue 是如何實現響應式系統的?
Vue 是如何實現響應式系統的?
看一個簡單的小例子
假設我們購物車中有一個商品,單價 price
,數量 quantity
,總價為 total
。
有這樣一個簡單的功能,點選按鈕讓 price
發生變化,那麼 total
為計算屬性,也隨之發生變化。
<!-- App.vue -->
<div id="app">
<div>Price: ¥{{ price }}</div>
<div>Quantity: {{ quantity }}</div>
<div>Total: ¥{{ total }}</div>
<div>
<button @click="price = 10">
change price
</button>
</div>
</div>
複製程式碼
// main.js
new Vue({
el: '#app',
data: {
price: 6.00,
quantity: 2
},
computed: {
total () {
return this.price * this.quantity
}
}
})
複製程式碼
考慮一下 Vue是如何實現這樣的功能的,其實我們是更改了資料,而依賴於它的計算屬性也發生了變化。
Vue 是如何實現追蹤資料的變化的
在官方文件,深入響應式原理裡有講到
當你把一個普通的 JavaScript 物件傳入 Vue 例項做為 data
選項,Vue 將遍歷此物件所有的屬性,並使用 Object.defineProperty
把這些屬性全部轉為 getter/setter。在內部它們讓 Vue 能夠追蹤依賴,在屬性被訪問和修改時通知變更。
每個元件例項都對應一個 watcher 例項,它會在元件渲染的過程中把“接觸”過的資料屬性記錄為依賴。之後當依賴項的 setter 觸發時,會通知 watcher,從而使它關聯的元件重新渲染。
一些弊端
受現代 JavaScript 的限制
對於已建立的例項,Vue **無法檢測到物件屬性的新增和刪除。**Vue 允許使用 Vue.set(object, propertyName, value)
的方法進行動態新增響應式屬性,或者使用別名寫成 this.$set(object, propertyName, value)
對於陣列,Vue 無法檢測以下陣列的變動:
- 當你利用索引直接設定一個陣列項時,例如:
vm.items[indexOfItem] = newValue
- 當你修改陣列的長度時,例如:
vm.items.length = newLength
第一條的解決方式同樣是使用 this.$set(vm.items, indexOfItem, newValue)
第二條的解決方式是用 vm.items.splice(indexOfItem, 1, newValue)
對於以上的弊端,其實官方已經給出瞭解決方法,就是使用 Proxy
代替 Object.defineProperty
API,這個改動會伴隨著萬眾期待的 Vue 3.0 的釋出而應用。大家都知道 Vue 2.x 也就是現在的版本,響應式是通過 Object.defineProperty
API來實現的,那麼既然知道了解決方法,我們不妨提前學習一下 Proxy
是如何做到響應式的。
使用 Proxy 實現響應式系統
Proxy 簡介
Proxy 物件用於定義基本操作的自定義行為(如屬性查詢,賦值,列舉,函式呼叫等)。
基本用法
let p = new Proxy(target, hanlder)
引數:
target
用 Proxy 包裝的目標物件(可以是任何型別的物件,包括原生陣列,函式,甚至另一個代理)。
handler
一個物件,其屬性是當執行一個操作時定義代理的行為的函式。
MDN 上的基礎示例:
let handler = {
get: function(target, name){
return name in target ? target[name] : 37;
}
};
let p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;
console.log(p.a, p.b); // 1, undefined
console.log('c' in p, p.c); // false, 37
複製程式碼
上述例子使用了 get
,當物件中不存在屬性名時,預設返回數為37。我們知道了對於操作物件,可以在使用 handler
處理一些事務。關於 handler
則有十三種。
實現一個響應式系統
重新回到我們的小例子,我們有一些變數,單價 price
,數量 quantity
,總價為 total
。我們來看下 JavaScript 是如何工作的
let price = 6
let quantity = 2
let total = 0
total = price * quantity
console.log(total) // 12
price = 10
console.log(total) // 12
複製程式碼
這好像不是我們的期望,我們期望的是,改變了 price,total的值也會更新。如何做到這一點,並不難。
let price = 6
let quantity = 2
let total = 0
total = price * quantity
console.log(total) // 12
price = 10
console.log(total) // 12
const updateTotal = () => {
total = price * quantity
}
updateTotal()
console.log(total) // 20
複製程式碼
我們定義了一個方法 updateTotal
,讓這個方法執行了我們需要更新 total
的業務邏輯,再重新執行這個方法那麼 total
的值就改變了。
我們可以考慮下我們想到達到什麼樣的目的,其實很簡單,就是當改變 price 或者 quantity 的時候 total 的值會跟著改變。學過設計模式的話,我們很容易想到這個場景符合觀察者模式。
使用觀察者模式
觀察者模式,它定義物件間的一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都將得到通知。
現在我們使用觀察者模式定義了觀察變數 price 與 quantity,如果它們的值發生變化,那麼依賴於它的計算屬性 total 將會得到一個 notify,這個 notify 即是我們的目標,去執行 updateTotal。
建立依賴類
把觀察者模式抽象為一個依賴類
// 代表依賴類
class Dep {
constructor () {
this.subscribers = [] // 把所有目標收集到訂閱裡
}
addSub (sub) { // 當有可觀察目標時,新增到訂閱裡
if (sub && !this.subscribers.includes(sub)) {
// 只新增未新增過的訂閱
this.subscribers.push(sub)
}
}
notify () { // 當被觀察的屬性發生變動時通知所有依賴於它的物件
this.subscribers.forEach(fn => fn()) // 重新執行所有訂閱過的目標方法
}
}
複製程式碼
那麼如何使變數 price 和 quantify 變得可觀察,在 Vue 2.x 中使用的是 Object.defineProperty
,本文會使用 Proxy
來實現。
// 使變數變為一個可觀察的物件的屬性
const dataObj = {
price: 6,
quantity: 2
}
let total = 0
let target = null
class Dep { // 代表依賴類
...
}
const dep = new Dep()
data = new Proxy(dataObj, {
get (obj, key) {
dep.addSub(target) // 將目標新增到訂閱中
return obj[key]
},
set (obj, key, newVal) {
obj[key] = newVal // 將新的值賦值給舊的值,引起值的變化
dep.notify() // 被觀察的屬性值發生變化,即通知所有依賴於它的物件
}
})
total = data.price * data.quantity
console.log(total) // 12
data.price = 10
console.log(total) // 12
target = () => {
total = data.price * data.quantity
}
target()
target = null
console.log(total) // 20
複製程式碼
上面程式碼稍微有些凌亂,我們重構一下,觀察者模式結合 Proxy 最終目的就是輸出被觀察的物件。我們可以抽象為一個 observer
使用 Proxy 實現觀察者模式
// 將依賴類與 Proxy 封裝為 observer,輸入一個普通物件,輸出為被觀察的物件
const observer = dataObj => {
const dep = new Dep()
return new Proxy(dataObj, {
get (obj, key) {
dep.addSub(target) // 將目標新增到訂閱中
return obj[key]
},
set (obj, key, newVal) {
obj[key] = newVal // 將新的值賦值給舊的值,引起值的變化
dep.notify() // 被觀察的屬性值發生變化,即通知所有依賴於它的物件
}
})
}
const data = observer(dataObj)
複製程式碼
我們注意到,其實每次我們還要重新執行我們的目標 target ,讓 total 值發生變化。這塊兒邏輯我們可以抽象為一個 watcher
,讓它幫我們做一些重複做的業務邏輯。
建立 watcher
const watcher = fn => {
target = fn
target()
target = null
}
watcher(() => {
total = data.price * data.quantity
})
複製程式碼
我們最終程式碼優化為:
// 使變數變為一個可觀察的物件的屬性
const dataObj = {
price: 6,
quantity: 2
}
let total = 0
let target = null
// 代表依賴類
class Dep {
...
}
// 使用 Proxy 實現了觀察者模式
const observer = dataObj => {
...
}
const data = observer(dataObj)
// 高階函式,重複執行訂閱方法
const watcher = fn => {
...
}
watcher(() => {
total = data.price * data.quantity
})
console.log(total) // 12
data.price = 30
console.log(total) // 60
複製程式碼
我們最終實現了開始的想法,total 會根據 price 值的改變而改變。實現了簡單的響應式系統。
為了縮小篇幅,上面的方法同時也有講過,即摺疊(簡化)起來,我們再回到 Vue 是如何追蹤資料的依賴的那張圖。再看看我們是如何實現的。
總結一下
通過小例子我們學習到的內容
- 我們學習到了通過建立一個 Dep 依賴類,來收集依賴關係,在訂閱者屬性被改變時,所有依賴於它的物件得以得到一個通知。
- 結合 Dep 依賴類,我們使用 Proxy 實現了觀察者模式的 observer 方法
- 我們建立了一個 watcher 觀察者方便管理我們要重新執行的業務邏輯,即我們新增到訂閱裡需要執行的方法
結尾
本文講到了 Vue2.x 中響應式系統的一些弊端,在即將到來的 Vue 3.0 Updates 中都將得到解決,在去年這時候尤大在 Plans for the Next Iteration of Vue.js 這篇博文中也有提到過。讓我們期待 Vue 3.0 的到來吧。