不久前,也就是11月14日-16日於多倫多舉辦的 VueConf TO 2018
大會上,尤雨溪發表了名爲 Vue3.0 Updates
的主題演講,對 Vue3.0
的更新計劃、方向進行了詳細闡述(感興趣的小夥伴能夠看看完整的 PPT),表示已經放棄使用了 Object.defineProperty
,而選擇了使用更快的原生 Proxy
!!html
這將會消除了以前 Vue2.x
中基於 Object.defineProperty
的實現所存在的不少限制:沒法監聽 屬性的添加和刪除、數組索引和長度的變動,並能夠支持 Map
、Set
、WeakMap
和 WeakSet
!前端
作爲一個 「前端工程師」 ,有必要安利一波 Proxy
!!api
MDN 上是這麼描述的——Proxy
對象用於定義基本操做的自定義行爲(如屬性查找,賦值,枚舉,函數調用等)。數組
官方的描述老是言簡意賅,以致於不明覺厲...bash
其實就是在對目標對象的操做以前提供了攔截,能夠對外界的操做進行過濾和改寫,修改某些操做的默認行爲,這樣咱們能夠不直接操做對象自己,而是經過操做對象的代理對象來間接來操做對象,達到預期的目的~前端工程師
什麼?還沒表述清楚?下面咱們看個例子,就一目瞭然了~app
let obj = {
a : 1
}
let proxyObj = new Proxy(obj,{
get : function (target,prop) {
return prop in target ? target[prop] : 0
},
set : function (target,prop,value) {
target[prop] = 888;
}
})
console.log(proxyObj.a); // 1
console.log(proxyObj.b); // 0
proxyObj.a = 666;
console.log(proxyObj.a) // 888
複製代碼
上述例子中,咱們事先定義了一個對象 obj
, 經過 Proxy
構造器生成了一個 proxyObj
對象,並對其的 set
(寫入) 和 get
(讀取) 行爲從新作了修改。ide
當咱們訪問對象內本來存在的屬性時,會返回原有屬性內對應的值,若是試圖訪問一個不存在的屬性時,會返回0
,即咱們訪問 proxyObj.a
時,本來對象中有 a
屬性,所以會返回 1
,當咱們試圖訪問對象中不存在的 b
屬性時,不會再返回 undefined
,而是返回了 0
,當咱們試圖去設置新的屬性值的時候,老是會返回 888
,所以,即使咱們對 proxyObj.a
賦值爲 666
,可是並不會生效,依舊會返回 888
!函數
ES6
原生提供的 Proxy
語法很簡單,用法以下:佈局
let proxy = new Proxy(target, handler);
參數 target
是用 Proxy
包裝的目標對象(能夠是任何類型的對象,包括原生數組,函數,甚至另外一個代理), 參數 handler
也是一個對象,其屬性是當執行一個操做時定義代理的行爲的函數,也就是自定義的行爲。
Proxy
的基本用法就如同上面這樣,不一樣的是 handler
對象的不一樣,handler
能夠是空對象 {}
,則表示對 proxy
操做就是對目標對象 target
操做,即:
let obj = {}
let proxyObj = new Proxy(obj,{})
proxyObj.a = 1;
proxyObj.fn = function () {
console.log('it is a function')
}
console.log(proxyObj.a); // 1
console.log(obj.a); // 1
console.log(obj.fn()) // it is a function
複製代碼
可是要注意的是,handler
不能 設置爲 null
,會拋出一個錯誤——Cannot create proxy with a non-object as target or handler
!
要想 Proxy
起做用,咱們就不能去操做原來對象的對象,也就是目標對象 target
(上例是 obj
對象 ),必須針對的是 Proxy
實例(上例是 proxyObj
對象)進行操做,不然達不到預期的效果,以剛開始的例子來看,咱們設置 get
方法後,試圖繼續從原對象 obj
中讀取一個不存在的屬性 b
, 結果依舊返回 undefined
:
console.log(proxyObj.b); // 0
console.log(obj.b); // undefined
複製代碼
對於能夠設置、但沒有設置攔截的操做,則對 proxy
對象的處理結果也一樣會做用於原來的目標對象 target
上,怎麼理解呢?仍是以剛開始的例子來看,咱們從新定義了 set
方法,全部的屬性設置都返回了 888
, 並無對某個特殊的屬性(這裏指的是 obj
的 a
屬性 )作特殊的攔截或處理,那麼經過 proxyObj.a = 666
操做後的結果一樣也會做用於原來目標對象(obj
對象)上,所以 obj
對象的 a
的值也將會變爲 888
!
proxyObj.a = 666;
console.log( proxyObj.a); // 888
console.log( obj.a); // 888
複製代碼
ES6
中 Proxy
目前提供了 13 種可代理操做,下面我對幾個比較經常使用的 api
作一些概括和整理,想要了解其餘方法的同窗可自行去官網查閱 :
--handler.get(target,property,receiver)
用於攔截對象的讀取屬性操做,target
是指目標對象,property
是被獲取的屬性名 , receiver
是 Proxy
或者繼承 Proxy
的對象,通常狀況下就是 Proxy
實例。
let proxy = new Proxy({},{
get : function (target,prop) {
console.log(`get ${prop}`);
return 10;
}
})
console.log(proxy.a) // get a
// 10
複製代碼
咱們攔截了一個空對象的 讀取get
操做, 當獲取其內部的屬性是,會輸出 get ${prop}
, 並返回 10 ;
let proxy = new Proxy({},{
get : function (target,prop,receiver) {
return receiver;
}
})
console.log(proxy.a) // Proxy{}
console.log(proxy.a === proxy) //true
複製代碼
上述 proxy
對象的 a
屬性是由 proxy
對象提供的,因此 receiver
指向 proxy
對象,所以 proxy.a === proxy
返回的是 true
。
要注意,若是要訪問的目標屬性是不可寫以及不可配置的,則返回的值必須與該目標屬性的值相同,也就是不能對其進行修改,不然會拋出異常~
let obj = {};
Object.defineProperty(obj, "a", {
configurable: false,
enumerable: false,
value: 10,
writable: false
});
let proxy = new Proxy(obj,{
get : function (target,prop) {
return 20;
}
})
console.log(proxy.a) // Uncaught TypeError
複製代碼
上述 obj
對象中的 a
屬性不可寫,不可配置,咱們經過 Proxy
建立了一個 proxy
的實例,並攔截了它的 get
操做,當咱們輸出 proxy.a
時會拋出異常,此時,若是咱們將 get
方法的返回值修改跟目標屬性的值相同時,也就是 10 , 就能夠消除異常~
--handler.set(target, property, value, receiver)
用於攔截設置屬性值的操做,參數於 get
方法相比,多了一個 value
,即要設置的屬性值~
在嚴格模式下,set
方法須要返回一個布爾值,返回 true
表明這次設置屬性成功了,若是返回false
且設置屬性操做失敗,而且會拋出一個TypeError
。
let proxy = new Proxy({},{
set : function (target,prop,value) {
if( prop === 'count' ){
if( typeof value === 'number'){
console.log('success')
target[prop] = value;
}else{
throw new Error('The variable is not an integer')
}
}
}
})
proxy.count = '10'; // The variable is not an integer
proxy.count = 10; // success
複製代碼
上述咱們經過修改 set
方法,對 目標對象中的 count
屬性賦值作了限制,咱們要求 count
屬性賦值必須是一個 number
類型的數據,若是不是,就返回一個錯誤 The variable is not an integer
,咱們第一次爲 count
賦值字符串 '10'
, 拋出異常,第二次賦值爲數字 10
, 打印成功,所以,咱們能夠用 set
方法來作一些數據校驗!
一樣,若是目標屬性是不可寫及不可配置的,則不能改變它的值,即賦值無效,以下:
let obj = {};
Object.defineProperty(obj, "count", {
configurable: false,
enumerable: false,
value: 10,
writable: false
});
let proxy = new Proxy(obj,{
set : function (target,prop,value) {
target[prop] = 20;
}
})
proxy.count = 20 ;
console.log(proxy.count) // 10
複製代碼
上述 obj
對象中的 count
屬性,咱們設置它不可被修改,而且默認值,咱們給定爲 10
,那麼即便給其賦值爲 20
,結果仍舊沒有變化!
--handler.apply(target, thisArg, argumentsList)
用於攔截函數的調用,共有三個參數,分別是目標對象(函數)target
,被調用時的上下文對象 thisArg
以及被調用時的參數數組 argumentsList
,該方法能夠返回任何值。
target
必須是是一個函數對象,不然將拋出一個TypeError
;
function sum(a, b) {
return a + b;
}
const handler = {
apply: function(target, thisArg, argumentsList) {
console.log(`Calculate sum: ${argumentsList}`);
return target(argumentsList[0], argumentsList[1]) * 2;
}
};
let proxy = new Proxy(sum, handler);
console.log(sum(1, 2)); // 3
console.log(proxy(1, 2)); // Calculate sum:1,2
// 6
複製代碼
實際上,apply
還會攔截目標對象的 Function.prototype.apply()
和 Function.prototype.call()
,以及 Reflect.apply()
操做,以下:
console.log(proxy.call(null, 3, 4)); // Calculate sum:3,4
// 14
console.log(Reflect.apply(proxy, null, [5, 6])); // Calculate sum: 5,6
// 22
複製代碼
--handler.construct(target, argumentsList, newTarget)
construct
用於攔截 new
操做符,爲了使 new
操做符在生成的 Proxy
對象上生效,用於初始化代理的目標對象自身必須具備[[Construct]]
內部方法;它接收三個參數,目標對象 target
,構造函數參數列表 argumentsList
以及最初實例對象時,new
命令做用的構造函數,即下面例子中的 p
。
let p = new Proxy(function() {}, {
construct: function(target, argumentsList, newTarget) {
console.log(newTarget === p ); // true
console.log('called: ' + argumentsList.join(', ')); // called:1,2
return { value: ( argumentsList[0] + argumentsList[1] )* 10 };
}
});
console.log(new p(1,2).value); // 30
複製代碼
另外,該方法必須返回一個對象,不然會拋出異常!
var p = new Proxy(function() {}, {
construct: function(target, argumentsList, newTarget) {
return 2
}
});
console.log(new p(1,2)); // Uncaught TypeError
複製代碼
--handler.has(target,prop)
has
方法能夠看做是針對 in
操做的鉤子,當咱們判斷對象是否具備某個屬性時,這個方法會生效,典型的操做就是 in
,改方法接收兩個參數 目標對象 target
和 要檢查的屬性 prop
,並返回一個 boolean
值。
let p = new Proxy({}, {
has: function(target, prop) {
if( prop[0] === '_' ) {
console.log('it is a private property')
return false;
}
return true;
}
});
console.log('a' in p); // true
console.log('_a' in p ) // it is a private property
// false
複製代碼
上述例子中,咱們用 has
方法隱藏了屬性如下劃線_
開頭的私有屬性,這樣在判斷時候就會返回 false
,從而不會被 in
運算符發現~
要注意,若是目標對象的某一屬性自己不可被配置,則該屬性不可以被代理隱藏,若是目標對象爲不可擴展對象,則該對象的屬性不可以被代理隱藏,不然將會拋出 TypeError
。
let obj = { a : 1 };
Object.preventExtensions(obj); // 讓一個對象變的不可擴展,也就是永遠不能再添加新的屬性
let p = new Proxy(obj, {
has: function(target, prop) {
return false;
}
});
console.log('a' in p); // TypeError is thrown
複製代碼
上面介紹了這麼多,也算是對 Proxy
又來一個初步的瞭解,那麼咱們就能夠利用 Proxy
手動實現一個極其簡單數據的雙向綁定(Object.defineProperty()
的實現方式能夠參考我上篇文章的末尾有涉及到)~
主要看功能的實現,因此佈局方面我就隨手一揮了~
頁面結構以下:
<!--html-->
<div id="app">
<h3 id="paragraph"></h3>
<input type="text" id="input"/>
</div>
複製代碼
主要仍是得看邏輯部分:
//獲取段落的節點
const paragraph = document.getElementById('paragraph');
//獲取輸入框節點
const input = document.getElementById('input');
//須要代理的數據對象
const data = {
text: 'hello world'
}
const handler = {
//監控 data 中的 text 屬性變化
set: function (target, prop, value) {
if ( prop === 'text' ) {
//更新值
target[prop] = value;
//更新視圖
paragraph.innerHTML = value;
input.value = value;
return true;
} else {
return false;
}
}
}
//添加input監聽事件
input.addEventListener('input', function (e) {
myText.text = e.target.value; //更新 myText 的值
}, false)
//構造 proxy 對象
const myText = new Proxy(data,handler);
//初始化值
myText.text = data.text;
複製代碼
上述咱們經過Proxy
建立了 myText
實例,經過攔截 myText
中 text
屬性 set
方法,來更新視圖變化,實現了一個極爲簡單的 雙向數據綁定~
說了這麼多 , Proxy
總算是入門了,雖然它的語法很簡單,可是要想實際發揮出它的價值,可不是件容易的事,再加上其自己的 Proxy
的兼容性方面的問題,因此咱們實際應用開發中使用的場景的並非不少,但不表明它不實用,在我看來,能夠利用它進行數據的二次處理、能夠進行數據合法性的校驗,甚至還能夠進行函數的代理,更多有用的價值等着你去開發呢~
何況,Vue3.0
都已經準備發佈了,你還不打算讓學習一下?
加油!