眾所周知,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 是一個可實例化的類