1. 程式人生 > >proxy寫監聽方法,實現響應式

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 = 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);
deps.set('discount', new Dep())
data['discount'] = 5;
let salePrice = 0;
watcher(() => {
 salePrice = data.price - data.discount;
});
console.log("salePrice = " + salePrice);
data.discount = 7.5
console.log("salePrice = " + salePrice);

總結

我們大致知道了Vue在未來版本如何使用Proxy來實現響應式,也瞭解到:

  • 當前響應式引擎的侷限性

  • Proxy是如何工作的

  • 如何使用Proxy來搭建一個響應式引擎