proxy寫監聽方法,實現響應式
var data = { price: 5, quantity: 2 };var data_without_proxy = data; // 儲存源物件
data = new Proxy(data_without_proxy, {
// 重寫資料以在中間建立一個代理
get(obj, key) {
console.log(obj+'取值')
},
set(obj, key, newVal) {
console.log(obj+"設定值")
}
});
data.price = 8 //設定
data.price //取值
【第1420期】JavaScript 響應式與 Proxy
前言
前端的溫度下降了嗎?今日早讀文章由@李金超推薦,汽車之家@花生翻譯分享。
@花生,就職於汽車之家使用者產品中心團隊,云云搬磚碼農的中的一員。
正文從這開始~~
Vue與Proxy
在之前的文章中,我們模擬了Vue的響應式引擎。使用Object.defineProperty()通過getters/setters實現屬性的響應性。
如果你一直有關注Vue的發展,就會發現2.x-next版本開始其響應式引擎將使用Proxy重寫,這就與我們之前講的不同了。
1-1響應式引擎利用代理進行重寫——標黃線的(譯者注)。
I wanted to ask Evan what exactly this might look like and the advantages we get from it(當成一句玩笑吧).
這樣做的好處
Proxy允許我們建立一個物件的虛擬代理(替代物件),併為我們提供了在訪問或修改原始物件時,可以進行攔截的處理方法(handler),如set()、get()和deleteProperty()等等。這樣我們就可以避免很常見的這兩種限制:
-
新增新的響應性屬性要使用Vue.$set(),刪除現有的響應性屬性要使用Vue.$delete()。
-
陣列的更新檢測。
之前的程式碼
我們之前使用Object.defineProperty()來實現監聽屬性的訪問和修改這兩種操作,程式碼如下:
let data = { price: 5, quantity: 2 }
let target = null
class Dep {
constructor () {
this.subscribers = []
}
depend () {
if (target && !this.subscribers.includes(target)) {
this.subscribers.push(target)
}
}
notify () {
this.subscribers.forEach(sub => sub())
}
}
Object.keys(data).forEach(key => {
let internalValue = data[key]
const dep = new Dep()
Object.defineProperty(data, key, {
get() {
dep.depend()
return internalValue
},
set(newVal) {
internalValue = newVal
dep.notify()
}
})
})
function watcher(myFun) {
target = myFun
target()
target = null
}
watcher(() => {
data.total = data.price * data.quantity
})
console.log("total = " + data.total)
data.price = 20
console.log("total = " + data.total)
data.quantity = 10
console.log("total = " + data.total)
使用Proxy克服限制
我們可以使用以下方法在data物件上建立一個代理,而不是遍歷每個屬性來新增getter/setter。
// data 是我們準備要建立代理的源物件
const observedData = new Proxy(data, {
get() {
// 訪問源物件屬性時呼叫
},
set() {
// 修改源物件屬性時呼叫
},
deleteProperty() {
// 刪除源物件屬性時呼叫
}
});
傳遞給Proxy建構函式的第二個引數可稱為處理方法(handler),這是一個包含了陷阱(套路)函式的物件,可以使我們能夠攔截髮生在源物件上的操作。
get()和set()就是兩個陷阱,分別在呼叫dep.depend()和dep.notify()時觸發。對於新新增的屬性,也會呼叫set(),這樣新新增的屬性同樣存在響應性。因此,我們不再需要使用Vue.$set()來新增新的響應性屬性。同理,deleteProperty()同樣適用。
使用Proxy實現響應式
儘管Proxy還沒有被整合到Vue的響應引擎中,但是我們可以嘗試一下,使用Proxy來實現之前文章中的例子。首先要更改的是Object.keys(data).forEach,我們現在將使用它為每個響應性屬性建立一個新的依賴例項:
let deps = new Map(); // 建立一個Map物件
Object.keys(data).forEach(key => {
// 為每個屬性都設定一個依賴例項 並放入 deps 中
deps.set(key, new Dep());
});
class Dep {
constructor () {
this.subscribers = []
}
depend () {
if (target && !this.subscribers.includes(target)) {
this.subscribers.push(target)
}
}
notify () {
this.subscribers.forEach(sub => sub())
}
}
let data_without_proxy = data; // 儲存源物件
data = new Proxy(data_without_proxy, {
// 重寫資料以在中間建立一個代理
get(obj, key) {
deps.get(key).depend(); // <-- 依舊為儲存target
return obj[key]; // 返回原始資料
},
set(obj, key, newVal) {
obj[key] = newVal; // 將原始資料設定為新值
deps.get(key).notify(); // <-- 依舊為重新執行已儲存的targets
return true;
}
});
注意:Dep class並不需要改動。單純使用Proxy替換Object.defineProperty。
如你所見,我們建立了一個變數data_without_proxy來作為源物件的副本,在覆蓋源物件時來使用副本建立一個Proxy物件。第二個引數是包含了get()和set()這兩個陷阱函式屬性的handler物件。
get(obj, key) => 是在訪問屬性時呼叫的函式。第一個引數obj為原始物件(data_without_proxy),第二個引數是被訪問屬性的key。這裡面呼叫了與特定屬性關聯的特定方法(Dep class中的depend())。最後,使用return obj[key]返回與該key相關的值。
set(obj, key, newVal) => 中前兩個引數與get的相同,第三個引數是新的修改值,然後,我們將新值設定給obj[key] = newVal修改的屬性上,並呼叫notify()方法。
調整Total並測試
我們需要對程式碼再做一個小小的修改,將total提取到它自己的變數中,因為它不需要存在響應性:
let total = 0;
watcher(() => {
total = data.price * data.quantity;
});
console.log("total = " + total);
data.price = 20;
console.log("total = " + total);
data.quantity = 10;
console.log("total = " + total);
現在,重新執行,我們會在控制檯中看到如下:
total = 10
total = 40
total = 200
這是個不錯的進展,當我們更新price和quantity時,total更新。
新增新的響應性屬性
現在,我們應該可以在不事先宣告屬性的情況下,將屬性新增到data中。這可能就是使用Proxy,而不是Object.defineProperty()的原因之一。我們可以新增如下程式碼,來嘗試一下:
deps.set("discount", new Dep()); // 為dep新增一個新屬性
data["discount"] = 5; // 為data新增同樣的新屬性
let salePrice = 0;
watcher(() => { // 對其進行監聽,其中包括我們新新增的屬性
salePrice = data.price - data.discount;
});
console.log("salePrice = " + salePrice);
data.discount = 7.5; // 此時就會呼叫我們的監聽函式,達到響應式的目的
console.log("salePrice = " + salePrice);
執行後,我們可以看到如下輸出:
salePrice = 15
salePrice = 12.5
可以看到,當data.discount被修改時,salePrice也會更新。下面為完整的程式碼:
let data = { price: 5, quantity: 2 };
let target = null;
class Dep {
constructor() {
this.subscribers = [];
}
depend() {
if (target && !this.subscribers.includes(target)) {
this.subscribers.push(target);
}
}
notify() {
this.subscribers.forEach(sub => sub());
}
}
// 前邊的程式碼都沒變
let deps = new Map(); // 建立一個Map物件
Object.keys(data).forEach(key => {
// 為每個屬性都設定一個依賴例項 並放入 deps 中
deps.set(key, new Dep());
});
let data_without_proxy = data; // 儲存源物件
data = new Proxy(data_without_proxy, {
// 重寫資料以在中間建立一個代理
get(obj, key) {
deps.get(key).depend(); // <-- 依舊為儲存target
return obj[key]; // 返回原始資料
},
set(obj, key, newVal) {
obj[key] = newVal; // 將原始資料設定為新值
deps.get(key).notify(); // <-- 依舊為重新執行已儲存的targets
return true;
}
});
// 用來監聽具有響應性屬性的程式碼
function watcher(myFunc) {
target = myFunc;
target();
target = null;
}
let total =