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

不久前,也就是11月14日-16日於多倫多舉辦的 VueConf TO 2018
大會上,尤雨溪發表了名為 Vue3.0 Updates
的主題演講,對 Vue3.0
的更新計劃、方向進行了詳細闡述(感興趣的小夥伴可以看看完整的 ofollow,noindex">PPT ),表示已經放棄使用了 Object.defineProperty
,而選擇了使用更快的原生 Proxy
!!
這將會消除了之前 Vue2.x
中基於 Object.defineProperty
的實現所存在的很多限制:無法監聽 屬性的新增和刪除 、 陣列索引和長度的變更 ,並可以支援 Map
、 Set
、 WeakMap
和 WeakSet
!
做為一個 “前端工程師” ,有必要安利一波 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);// 1 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 複製程式碼
API
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]) * 10; } }; 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
都已經準備釋出了,你還不打算讓學習一下?
加油!