初探 Vue3.0 中的一大亮點——Proxy !

前言

不久前,也就是11月14日-16日於多倫多舉辦的 VueConf TO 2018 大會上,尤雨溪發表了名爲 Vue3.0 Updates 的主題演講,對 Vue3.0 的更新計劃、方向進行了詳細闡述(感興趣的小夥伴能夠看看完整的 PPT),表示已經放棄使用了 Object.defineProperty,而選擇了使用更快的原生 Proxy !!html

這將會消除了以前 Vue2.x 中基於 Object.defineProperty 的實現所存在的不少限制:沒法監聽 屬性的添加和刪除數組索引和長度的變動,並能夠支持 MapSetWeakMapWeakSet前端

作爲一個 「前端工程師」 ,有必要安利一波 Proxy !!api

什麼是 Proxy?

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 , 並無對某個特殊的屬性(這裏指的是 obja 屬性 )作特殊的攔截或處理,那麼經過 proxyObj.a = 666 操做後的結果一樣也會做用於原來目標對象(obj 對象)上,所以 obj 對象的 a 的值也將會變爲 888 !

proxyObj.a = 666;
    console.log( proxyObj.a);   // 888
    console.log( obj.a);        // 888
複製代碼

API

ES6Proxy 目前提供了 13 種可代理操做,下面我對幾個比較經常使用的 api 作一些概括和整理,想要了解其餘方法的同窗可自行去官網查閱 :

--handler.get(target,property,receiver)

用於攔截對象的讀取屬性操做,target 是指目標對象,property 是被獲取的屬性名 , receiverProxy 或者繼承 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 實例,經過攔截 myTexttext 屬性 set 方法,來更新視圖變化,實現了一個極爲簡單的 雙向數據綁定~

總結

說了這麼多 , Proxy 總算是入門了,雖然它的語法很簡單,可是要想實際發揮出它的價值,可不是件容易的事,再加上其自己的 Proxy 的兼容性方面的問題,因此咱們實際應用開發中使用的場景的並非不少,但不表明它不實用,在我看來,能夠利用它進行數據的二次處理、能夠進行數據合法性的校驗,甚至還能夠進行函數的代理,更多有用的價值等着你去開發呢~

何況,Vue3.0 都已經準備發佈了,你還不打算讓學習一下?

加油!

相關文章
相關標籤/搜索