寫在前面: 11月16日早上,Vue.js的作者尤大大在 Vue Toronto 的主題演講中預演了 Vue.js 3.0的一些新特性,其中一個很重要的改變就是Vue3 將使用 ES6的Proxy 作為其觀察者機制,取代之前使用的Object.defineProperty。我相信許多同學深有體會,許多面試中Object.defineProperty是vue這個框架一個出現率很高的考察點,一開始大家對這個屬性還有點陌生,慢慢的隨著使用vue的人越來越多,這個屬性經常被大家拿來研究,而就在大家漸漸熟悉了這個屬性以後,vue的作者打算在下個vue版本中用 Proxy替換它,果然一入前端坑就爬不出來了哈哈。雖然vue3正式釋出要等到明年下半年了,但我們下面可以來探索下基於 Proxy 的觀察者機制,預測下vue3關於Proxy這部分的程式碼(雖然看上去並沒有什麼用哈哈)。
一.為什麼要取代Object.defineProperty
既然要取代Object.defineProperty,那它肯定是有一些明顯的缺點,總結起來大概是下面兩個:
- 在Vue中,Object.defineProperty無法監控到陣列下標的變化,導致直接通過陣列的下標給陣列設定值,不能實時響應。 為了解決這個問題,經過vue內部處理後可以使用以下幾種方法來監聽陣列 (評論區有提到,Object.defineProperty本身是可以監控到陣列下標的變化的,具體可參Vue為什麼不能檢測陣列變動)
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
複製程式碼
由於只針對了以上八種方法進行了hack處理,所以其他陣列的屬性也是檢測不到的,還是具有一定的侷限性。
- Object.defineProperty只能劫持物件的屬性,因此我們需要對每個物件的每個屬性進行遍歷。Vue裡,是通過遞迴以及遍歷data 物件來實現對資料的監控的,如果屬性值也是物件那麼需要深度遍歷,顯然如果能劫持一個完整的物件,不管是對操作性還是效能都會有一個很大的提升。
而要取代它的Proxy有以下兩個優點;
- 可以劫持整個物件,並返回一個新物件
- 有13種劫持操作
看到這可能有同學要問了,既然Proxy能解決以上兩個問題,而且Proxy屬性在vue2.x之前就有了,為什麼vue2.x不使用Proxy呢?一個很重要的原因就是:
- Proxy是es6提供的新特性,相容性不好,最主要的是這個屬性無法用polyfill來相容
經評論提醒,目前Proxy並沒有有效的相容方案,未來大概會是3.0和2.0並行,需要支援IE的選擇2.0
關於Object.defineProperty來實現觀察者機制,可以參照剖析Vue原理&實現雙向繫結MVVM這篇文章,下面的內容主要介紹如何基於 Proxy來實現vue觀察者機制。
二.什麼是Proxy
1.含義:
- Proxy是 ES6 中新增的一個特性,翻譯過來意思是"代理",用在這裡表示由它來“代理”某些操作。 Proxy 讓我們能夠以簡潔易懂的方式控制外部對物件的訪問。其功能非常類似於設計模式中的代理模式。
- Proxy 可以理解成,在目標物件之前架設一層“攔截”,外界對該物件的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。
- 使用 Proxy 的核心優點是可以交由它來處理一些非核心邏輯(如:讀取或設定物件的某些屬性前記錄日誌;設定物件的某些屬性值前,需要驗證;某些屬性的訪問控制等)。 從而可以讓物件只需關注於核心邏輯,達到關注點分離,降低物件複雜度等目的。
2.基本用法:
let p = new Proxy(target, handler);
複製程式碼
引數:
target
是用Proxy包裝的被代理物件(可以是任何型別的物件,包括原生陣列,函式,甚至另一個代理)。
handler
是一個物件,其宣告瞭代理target 的一些操作,其屬性是當執行一個操作時定義代理的行為的函式。
p
是代理後的物件。當外界每次對 p 進行操作時,就會執行 handler 物件上的一些方法。Proxy共有13種劫持操作,handler代理的一些常用的方法有如下幾個:
get:讀取
set:修改
has:判斷物件是否有該屬性
construct:建構函式
複製程式碼
3.示例:
下面就用Proxy來定義一個物件的get和set,作為一個基礎demo
let obj = {};
let handler = {
get(target, property) {
console.log(`${property} 被讀取`);
return property in target ? target[property] : 3;
},
set(target, property, value) {
console.log(`${property} 被設定為 ${value}`);
target[property] = value;
}
}
let p = new Proxy(obj, handler);
p.name = 'tom' //name 被設定為 tom
p.age; //age 被讀取 3
複製程式碼
p 讀取屬性的值時,實際上執行的是 handler.get() :在控制檯輸出資訊,並且讀取被代理物件 obj 的屬性。
p 設定屬性值時,實際上執行的是 handler.set() :在控制檯輸出資訊,並且設定被代理物件 obj 的屬性的值。
以上介紹了Proxy基本用法,實際上這個屬性還有許多內容,具體可參考Proxy文件
三.基於Proxy來實現雙向繫結
話不多說,接下來我們就來用Proxy來實現一個經典的雙向繫結todolist,首先簡單的寫一點html結構:
<div id="app">
<input type="text" id="input" />
<div>您輸入的是: <span id="title"></span></div>
<button type="button" name="button" id="btn">新增到todolist</button>
<ul id="list"></ul>
</div>
複製程式碼
先來一個Proxy,實現輸入框的雙向繫結顯示:
const obj = {};
const input = document.getElementById("input");
const title = document.getElementById("title");
const newObj = new Proxy(obj, {
get: function(target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function(target, key, value, receiver) {
console.log(target, key, value, receiver);
if (key === "text") {
input.value = value;
title.innerHTML = value;
}
return Reflect.set(target, key, value, receiver);
}
});
input.addEventListener("keyup", function(e) {
newObj.text = e.target.value;
});
複製程式碼
這裡程式碼涉及到Reflect
屬性,這也是一個es6的新特性,還不太瞭解的同學可以參考Reflect文件.
接下來就是新增todolist列表,先把陣列渲染到頁面上去:
// 渲染todolist列表
const Render = {
// 初始化
init: function(arr) {
const fragment = document.createDocumentFragment();
for (let i = 0; i < arr.length; i++) {
const li = document.createElement("li");
li.textContent = arr[i];
fragment.appendChild(li);
}
list.appendChild(fragment);
},
addList: function(val) {
const li = document.createElement("li");
li.textContent = val;
list.appendChild(li);
}
};
複製程式碼
再來一個Proxy,實現Todolist的新增:
const arr = [];
// 監聽陣列
const newArr = new Proxy(arr, {
get: function(target, key, receiver) {
return Reflect.get(target, key, receiver);
},
set: function(target, key, value, receiver) {
console.log(target, key, value, receiver);
if (key !== "length") {
Render.addList(value);
}
return Reflect.set(target, key, value, receiver);
}
});
// 初始化
window.onload = function() {
Render.init(arr);
};
btn.addEventListener("click", function() {
newArr.push(parseInt(newObj.text));
});
複製程式碼
這樣就用 Proxy實現了一個簡單的雙向繫結Todolist,具體程式碼可參考proxy.html
四.基於Proxy來實現vue的觀察者機制
1.Proxy實現observe
observe(data) {
const that = this;
let handler = {
get(target, property) {
return target[property];
},
set(target, key, value) {
let res = Reflect.set(target, key, value);
that.subscribe[key].map(item => {
item.update();
});
return res;
}
}
this.$data = new Proxy(data, handler);
}
複製程式碼
這段程式碼裡把代理器返回的物件代理到this.$data
,即this.$data
是代理後的物件,外部每次對this.$data
進行操作時,實際上執行的是這段程式碼裡handler物件上的方法。
2.compile和watcher
比較熟悉vue的同學都很清楚,vue2.x在 new Vue() 之後。 Vue 會呼叫 _init 函式進行初始化,它會初始化生命週期、事件、 props、 methods、 data、 computed 與 watch 等。其中最重要的是通過 Object.defineProperty 設定 setter 與 getter 函式,用來實現「響應式」以及「依賴收集」。類似於下面這個內部流程圖:
而我們上面已經用Proxy取代了Object.defineProperty這部分觀察者機制,而要實現整個基本mvvm雙向繫結流程,除了observe還需要compile和watche等一系列機制,我們這裡像模板編譯的工作就不展開描述了,為了實現基於Proxy的vue新增Totolist,這裡只寫了 compile和watcher來支援observe的工作,具體程式碼參考proxyVue,這個程式碼相當於一個基於Proxy的一個簡化版vue,主要是實現雙向繫結這個功能,為了方便這裡把js放到了html頁面中,大家本地執行後可以發現,現在的效果和第三章的效果達到一致了,等到明年vue3釋出,它原始碼裡基於 Proxy實現的的觀察者機制可能和這裡的實現會有很多不同,這篇文章主要是對 Proxy這個特性做了一些介紹以及它的一些應用,而作者本人也通過對Proxy 的觀察者機制探索學到了不少東西,所以整合資源,總結出了這篇文章,希望能和大家共勉之,以上,我們下次有緣再見。