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

Chris威發表於2018-12-05

前言

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

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

這將會消除了之前 Vue2.x 中基於 Object.defineProperty 的實現所存在的很多限制:無法監聽 屬性的新增和刪除陣列索引和長度的變更,並可以支援 MapSetWeakMapWeakSet

做為一個 “前端工程師” ,有必要安利一波 Proxy !!

什麼是 Proxy?

MDN 上是這麼描述的——Proxy物件用於定義基本操作的自定義行為(如屬性查詢,賦值,列舉,函式呼叫等)。

官方的描述總是言簡意賅,以至於不明覺厲...

其實就是在對目標物件的操作之前提供了攔截,可以對外界的操作進行過濾和改寫,修改某些操作的預設行為,這樣我們可以不直接操作物件本身,而是通過操作物件的代理物件來間接來操作物件,達到預期的目的~

什麼?還沒表述清楚?下面我們看個例子,就一目瞭然了~

    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 (讀取) 行為重新做了修改。

當我們訪問物件內原本存在的屬性時,會返回原有屬性內對應的值,如果試圖訪問一個不存在的屬性時,會返回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 都已經準備釋出了,你還不打算讓學習一下?

加油!

相關文章