<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
最近公司專案從vue2
遷移到vue3
,感覺自己對Object.defineProperty和Proxy的瞭解還是在淺嘗輒止的地步,所以今天抽空整體對二者進行了深入(基礎)的瞭解,主要是二者的基礎用法,效能對比,在vue中的應用進行了探索,希望能夠幫助到想了解的小夥伴。
首先,來看看MDN上的定義:
Object.defineProperty()
方法會直接在一個物件上定義一個新屬性,或者修改一個物件的現有屬性,並返回此物件。
備註: 應當直接在 Object 構造器物件上呼叫此方法,而不是在任意一個 Object
型別的範例上呼叫。
Object.defineProperty(obj, prop, descriptor)
引數說明:
obj
:要定義屬性的物件。
prop
:要定義或修改的屬性的名稱或Symbol
。
descriptor
:要定義或修改的屬性描述符。
let person = {}; let name = 'yuanwill'; Object.defineProperty(person, 'name', { get() { return name === 'yuanwill' ? 'zhangsan' : 'lisi' }, set(newVal) { name = newVal } }); console.log(person.name); // zhangsan person.name = 'haha'; console.log(person.name); // lisi
person
的name
屬性時,存取了get
方法,第一次name
是yuanwill
,所以返回了zhangsan
person
的name
屬性時,存取了set
方法,修改了name
變數的值person
的name
屬性,同理,返回了lisi
。在vue2中,使用了Object.defineProperty
來實現資料雙向繫結的基礎(具體的observe,watcher,dep等等balabala就不細說了),我們主要仿造vue
來看看怎麼通過Object.defineProperty
來實現一個物件或陣列(不扯對陣列方法的攔截AOP)的屬性攔截和監聽。
準備一個物件如下:
let person = { name: 'yuanwill', age: 26, address: { home: 'guangzhou', now: 'shenzhen' } };
很容易想到,我們需要遍歷person
中的key
,然後對每一個key
進行轉換即可,於是很自然的寫出了下面的錯誤範例:
Object.keys(person).forEach(key => { Object.defineProperty(person, key, { get() { console.log('攔截到正在獲取屬性:' + key); return person[key]; // ① }, set(val) { console.log('攔截到正在修改屬性:' + key); person[key] = val; // ② } }) }) console.log(person.name)
執行程式碼發現棧溢位了,錯誤有兩處,程式碼已經標明:
get
中,直接使用person[key] 會繼續呼叫get
,導致死迴圈set
中同理。所以,需要使用一個方法,來傳遞person[key] 的值。
const defineReactive = (obj, key, val) => { Object.defineProperty(obj, key, { get() { console.log('攔截到正在獲取屬性:' + key); return val; }, set(newVal) { console.log('攔截到正在修改屬性:' + key); val = newVal; } }) } const observer = obj => { // 如果obj不是一個物件,就沒必要包裝了 if(typeof obj !== 'object' || !obj) { return; } Object.keys(obj).forEach(key => { defineReactive(obj, key, obj[key]) }) }
實驗一下:
observer(person); person.name = 'haha'; // 攔截到正在修改屬性:name console.log(person.name); // 攔截到正在獲取屬性:name, haha
可是,還有瑕疵,比如:
person.name = { firstName: 'yuan', lastName: 'will' }; // 攔截到正在修改屬性:name person.name.firstName = 'haha'; // 攔截到正在獲取屬性:name console.log(person.name); // 攔截到正在獲取屬性:name
可以看到,person.name.firstName
並沒有攔截到正在修改firstName
屬性。原因是我們在set
的時候,newVal
可能也是一個object
,所以也需要進行observer
。
修改set如下:
set(newVal) { if(typeof newVal === 'object') { observer(newVal); } console.log('攔截到正在修改屬性:' + key); val = newVal; }
當然,還有瑕疵,比如存取深層物件:
console.log(person.address.home) // 攔截到正在獲取屬性:address
並沒有攔截到存取屬性home
,所以我們還需要判斷val
如果是物件也應該再一次observer
。優化後的完整程式碼如下:
const defineReactive = (obj, key, val) => { if(typeof val === 'object') { observer(val); } Object.defineProperty(obj, key, { get() { console.log('攔截到正在獲取屬性:' + key); return val; }, set(newVal) { if(typeof newVal === 'object') { observer(newVal); } console.log('攔截到正在修改屬性:' + key); val = newVal; } }) } const observer = obj => { // 如果obj不是一個物件,就沒必要包裝了 if(typeof obj !== 'object' || !obj) { return; } Object.keys(obj).forEach(key => { defineReactive(obj, key, obj[key]) }) }
我們總說,Object.defineProperty
不能攔截陣列,這種說法不太準確,看範例:
let list = [1,2,3,4]; observer(list); console.log(list[0]) // 攔截到正在獲取屬性:0 list[0] = 2; // 攔截到正在修改屬性:0 list[6] = 6; // 無法攔截... list.push(3); // 無法攔截...
可以看到,通過索引去存取或修改已經存在的元素,是可以攔截到的。如果是不存在的元素,或者是通過push
等方法去修改陣列,則無法攔截。
正因為如此,vue2在實現的時候,通過重寫了陣列原型上的七個方法(push、pop、shift、unshift、splice、sort、reverse)來解決(具體可以看vue/src/core/observer/array.js),就不展開了。
同樣,來看看MDN上的定義:
Proxy 物件用於建立一個物件的代理,從而實現基本操作的攔截和自定義(如屬性查詢、賦值、列舉、函數呼叫等)。
const p = new Proxy(target, handler)
引數說明:
target
:要使用 Proxy
包裝的目標物件(可以是任何型別的物件,包括原生陣列,函數,甚至另一個代理)。handler
:一個通常以函數作為屬性的物件,各屬性中的函數分別定義了在執行各種操作時代理 p
的行為。handler
物件總共有13個屬性方法,具體的可以參考MDN,就不一一列舉了。
let person = { name: 'yuanwill' } let personProxy = new Proxy(person, { get(target, key) { return target[key] === 'yuanwill' ? 'zhangsan': 'lisi' }, set(target, key, val) { target[key] = val; return true; } }); console.log(personProxy.name); // zhangsan personProxy.name = 'haha'; console.log(personProxy.name); // lisi
案例及其簡單,就不介紹了。
proxy
的攔截,並不是“萬事萬物”都攔截,看MDN上面的定義,是對基本操作的攔截和自定義,那麼何為基本操作呢,看下面的例子:
const person = { name: 'yuanwill', say() { console.log('你好呀') } } let personProxy = new Proxy(person, { get(target, key) { console.log('攔截到正在獲取屬性:' + key); return target[key] }, set(target, key, val) { console.log('攔截到正在修改屬性:' + key); target[key] = val; }, apply(target, thisArg, arguments) { console.log('攔截到了正在執行的方法:' + target); return target.call(thisArg, ...arguments) } }) console.log(personProxy.name); // 攔截到正在獲取屬性:name personProxy.name = 'haha'; // 攔截到正在修改屬性:name personProxy.say(); // 攔截到正在獲取屬性:say
重點在最後一句程式碼,發現personProxy.say()
並沒有走入apply
方法中,原因就在於只攔截基本操作。
那麼到底什麼是基本操作呢?像上面的personProxy.name
這種屬性的讀取,personProxy.name = 'haha'
這種屬性的賦值就是基本操作,而personProxy.say()
是由兩個基本操作(personProxy.say
的讀取以及函數的呼叫)組成的複合操作,我們代理的物件是person
,而不是person.say
,所以,我們只攔截到了person.say
的讀取操作。
來看看MDN上的定義:
Reflect 是一個內建的物件,它提供攔截 JavaScript 操作的方法。這些方法與proxy handlers (en-US)的方法相同。Reflect
不是一個函數物件,因此它是不可構造的。
換句話說,Reflet物件的方法和proxy的攔截器(第二個入參handler)的方法完全一致,因此也有13個方法,就不一一列舉了。
Reflect的作用也及其簡單,可以參考MDN上。
那麼,為什麼我們需要Reflect呢,來看下面的例子:
const person = { name: 'yuanwill', get firstName() { return this.name; } }; const personProxy = new Proxy(person, { get(target, key) { console.log('攔截到正在獲取屬性:' + key); return target[key] }, set(target, key, val) { console.log('攔截到正在修改屬性:' + key); target[key] = val; } }); console.log(personProxy.firstName); // 攔截到正在獲取屬性:firstName
按照我們的理解,應該還需要攔截到name
屬性,因為我們在firstName
中返回的是name
屬性,那麼為什麼沒有攔截到呢?關鍵在於this
指向問題,personProxy.firstName
會被get
攔截,然後返回target[key]
,這裡的target
就是person
,key
就是firstName
,所以這個時候的this.name
就是person.name
,而我們的代理物件是personProxy
,所以存取name
屬性就不會被攔截了。
那這個時候,Reflect
就派上用場了:
const personProxy = new Proxy(person, { get(target, key, receiver) { console.log('攔截到正在獲取屬性:' + key); return Reflect.get(target, key, receiver); }, set(target, key, val, receiver) { console.log('攔截到正在修改屬性:' + key); return Reflect.set(target, key, val, receiver); } });
這個時候,就能攔截到了。原因在於,Reflect.get
中的第三個引數receiver
作用就是改變this的指向,MDN描述如下:
如果target物件中指定了getter,receiver則為getter呼叫時的this值。
還是使用上面的物件:
let person = { name: 'yuanwill', age: 26, address: { home: 'guangzhou', now: 'shenzhen' } };
我們很自然就能寫出如下程式碼:
const observer = obj => { // 如果obj不是一個物件,就沒必要包裝了 if(typeof obj !== 'object' || !obj) { return obj; } const proxyConfig = { get(target, key, receiver) { console.log('攔截到正在獲取屬性:' + key); return Reflect.get(target, key, receiver) }, set(target, key, val, receiver) { console.log('攔截到正在修改屬性:' + key); return Reflect.set(target, key, val, receiver);; } }; const observed = new Proxy(obj, proxyConfig); return observed; }
測試一下:
const personProxy = observer(person); personProxy.name = 'haha'; // 攔截到正在修改屬性:name console.log(personProxy.name); // 攔截到正在獲取屬性:name
當然,也有瑕疵:
personProxy.name = { firstName: 'yuan', lastName: 'will' }; // 攔截到正在修改屬性:name personProxy.name.firstName = 'haha'; // 攔截到正在獲取屬性:name console.log(personProxy.name); // 攔截到正在獲取屬性:name
可以看到,person.name.firstName
依然沒有攔截到正在修改firstName屬性。原因在於,get
返回的可能是個物件,我們需要對這個物件再次代理,所以修改如下:
const observer = obj => { // 如果obj不是一個物件,就沒必要包裝了 if(typeof obj !== 'object' || !obj) { return obj; } const proxyConfig = { get(target, key, receiver) { console.log('攔截到正在獲取屬性:' + key); const result = Reflect.get(target, key, receiver); return observer(result); }, set(target, key, val, receiver) { console.log('攔截到正在修改屬性:' + key); return Reflect.set(target, key, val, receiver);; } }; const observed = new Proxy(obj, proxyConfig); return observed; }
仔細分析上面的程式碼,我們在get
的時候,才去判斷了獲取的值是不是一個物件,而Object.defineProperty
是最開始就回圈遍歷,對每個屬性進行代理,所以,這樣效能就提升了。同時,我們獲取personProxy.address.home
也能攔截到home
屬性了(想想就知道為啥了)。
let list = [1,2,3,4]; let listProxy = observer(list); console.log(listProxy[0]) // 攔截到正在獲取屬性:0 listProxy[0] = 2; // 攔截到正在修改屬性:0 listProxy[6] = 6; // 攔截到正在修改屬性:6 /** * 攔截到正在獲取屬性:push * 攔截到正在獲取屬性:length * 攔截到正在修改屬性:7 * 攔截到正在修改屬性:length */ listProxy.push(3);
可以看到,proxy天然的解決了陣列的相關問題。
Object.defineProperty
和Proxy
的相關基礎就介紹完了,文章只是講解了比較基礎的功能,學無止境,沒辦法了,慢慢來把~~~
更多關於defineProperty Proxy基礎功能的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45