眾所周知,Vue 2.x 的數據綁定是通過 defineProperty。而在 Vue 3.x 的設計中,數據綁定是通過 Proxy 實現的,這兩者到底有何異同? 一、definePropety defineProperty 是 Object 的一個方法,可以在對象上新增或編輯某個屬性,可編輯的內容 ...
眾所周知,Vue 2.x 的數據綁定是通過 defineProperty。而在 Vue 3.x 的設計中,數據綁定是通過 Proxy 實現的,這兩者到底有何異同?
一、definePropety
defineProperty 是 Object 的一個方法,可以在對象上新增或編輯某個屬性,可編輯的內容除了屬性值 value 之外,還有該屬性的描述信息
Object.defineProperty(obj, prop, descriptor)
該方法接收三個參數,分別是目標對象 obj,被編輯的屬性名 prop,以及該屬性的描述 descriptor
需要註意的是,只能在 Object 構造器對象使用該方法,實例化的 object 類型是沒有該方法的
1. 基礎描述符
- configurable: 當該鍵值為 true 時,該屬性的描述符才能夠被改變,同時該屬性也能從對應的對象上被刪除。預設為 false。
當該描述符為 false 的時候,其它的描述符一旦定義,就無法再更改,且該屬性無法被 delete 刪除
- enumerable: 當該鍵值為 true 時,該屬性才會出現在對象的枚舉屬性中。預設為 false。
當 enumerable 為 false 時,Objcet.keys() 和 for...in 都無法獲取到被定義的屬性
但 Reflect.ownKeys() 可以...
2. 數據描述符
- value: 屬性值。可以是任何有效的 JavaScript 值 (數值,對象,函數等)。預設為 undefined。
- writable: 當該鍵值為 true 時,屬性的值(即 value)才能被賦值運算符改變。 預設為 false。
3. 存取描述符
- get:該屬性的 getter 函數,訪問該屬性時候會調用該函數,其返回值會被用作 value,預設為 undefined。
該函數沒有入參,但是可以使用 this 對象,只是這個 this 不一定是源對象 obj
- set: 該屬性的 setter 函數,當屬性值被修改時,會調用此函數,預設為 undefined。
該方法接受一個參數,即被賦予的新值,同時會傳入賦值時的 this 對象
⚠️註意:數據描述符和存取描述符不可同時存在!
4. Vue 2.x 響應式原理
在 Vue 2.x 中其實就是在觀察者模式中使用上面提到的 get 和 set 實現的數據綁定
首先實現依賴收集和 Watcher
// 通過 Dep 解耦屬性的依賴和更新操作
class Dep {
constructor() {
this.subs = []
}
// 添加依賴
addSub(sub) {
this.subs.push(sub)
}
// 更新
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
// 全局屬性,通過該屬性配置 Watcher
Dep.target = null
class Watcher {
constructor(obj, key, up) {
// 手動觸發 getter 以添加監聽
Dep.target = this
this.up = up
this.obj = obj
this.key = key
this.value = obj[key]
// 完成依賴添加後重置 target
Dep.target = null
}
update() {
// 獲得新值
this.value = this.obj[this.key]
// 調用 update 方法更新 Dom
this.up(this.value)
}
}
然後通過 defineProperty 來實現響應
function observe(obj) {
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
function defineReactive(obj, key, val) {
// 遞歸子屬性
observe(val)
let dp = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 將 Watcher 添加到訂閱
if (Dep.target) {
dp.addSub(Dep.target)
}
return val
},
set(newVal) {
val = newVal
// 執行 watcher 的 update 方法
dp.notify()
}
})
}
完成之後,通過 observe 遍歷對象,然後實例化 Watcher,手動觸發一次 getter 完成數據綁定
const data = { name: '' }
observe(data)
function update(value) {
document.body.innerHTML = `<div>${value}</div>`
}
// 模擬解析到 `{{name}}` 觸發的操作
new Watcher(data, 'name', update)
data.name = 'Wise.Wrong'
這部分代碼參考自掘金小冊《前端面試之道》
二、Proxy
以 Object.defineProperty() 實現的響應式有兩個問題:
1. 給對象新增屬性並不會更新 DOM;
2. 以索引的方式修改數組也不會觸發 DOM 的更新。
最終 Vue 是通過重寫函數的方式解決了這兩個問題,但對於數組的數據綁定依然有瑕疵
而這些問題,對於 Proxy 來說都不是問題
1. 簡介
const p = new Proxy(target, handler)
這裡的目標對象 target 可以是任何類型的對象,包括原生數組,函數,甚至另一個 Proxy
而對應的處理器對象 handler 包含很多的 trap 方法,這些 trap 方法會在 Proxy 對象執行對應操作時觸發
下麵會介紹幾個常用的方法
getPrototypeOf() | Object.getPrototypeOf 方法對應的鉤子函數 |
setPrototypeOf() | Object.setPrototypeOf 方法對應的鉤子函數 |
defineProperty() | Object.defineProperty 方法對應的鉤子函數 |
has() | in 操作符對應的鉤子函數 |
deleteProperty() | delete 操作符對應的鉤子函數 |
apply() | 函數被調用時的鉤子函數 |
construct() | new 操作符對應的鉤子函數 |
get() | 屬性讀取操作的鉤子函數 |
set() | 屬性被修改時的鉤子函數 |
鉤子函數會在對 Proxy 對象執行相應操作的時候觸發
2. 鉤子函數
以 set 和 get 為例
function update(value = 'wise.wrong') {
console.log('update');
document.body.innerHTML = value;
};
const data = ['who', 'am', 'i'];
const subject = new Proxy(data, {
get: function(obj, prop) {
return obj[prop];
},
set: function(obj, prop, value) {
update(value);
obj[prop] = value;
}
});
上面的目標的對象是一個數組,然後實例化 Proxy 的時候添加了 set 的鉤子函數
當 Proxy 對象 subject 被修改的時候,會執行 update 方法
基於這些鉤子函數,就可以參考上面 Object.defineProperty() 的思路實現數據綁定了,而且還不會有上面的遺留問題
3. 和 defineProperty 的區別
defineProperty 需要針對具體的 key 設置 getter 和 setter
Object.defineProperty(obj, prop, descriptor)
以至於 Vue 2.x 在初始化的時候,需要遞歸遍歷對象的子屬性,挨個兒掛載 setter
這也導致了無法直接通過 defineProperty 實現在對象中新增屬性時更新 DOM
但 Proxy 是針對整個對象的代理,不會關心具體的 key
而且 Proxy 的目標對象並沒有類型限制,除了 Object 之外,還天然支持 Array、Function 的代理
此外 Proxy 還不僅僅支持 getter 和 setter,上面提到的鉤子函數 ,在特定的場景下會發揮出應有的作用
所以 Proxy 比 Object.defineProperty() 的層次更高,畢竟 defineProperty 只是一個方法,而 Proxy 是一個可實例化的類