一文搞懂 Vue3-0 爲什麼採用 Proxy
來自:掘金,作者:花椰菜菜
鏈接:https://juejin.cn/post/7069397770766909476
文章篇幅會比較長,但是看完一定會收穫滿滿~ 希望你堅持看下去呀~
Object.defineProperty()
作用:在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回這個對象。
- 基本使用
語法:Object.defineProperty(obj, prop, descriptor)
參數:
-
要添加屬性的對象
-
要定義或修改的屬性的名稱或 [
Symbol
] -
要定義或修改的屬性描述符
看一個簡單的例子
let person = {}
let personName = 'lihua'
//在person對象上添加屬性namep,值爲personName
Object.defineProperty(person, 'namep', {
//但是默認是不可枚舉的(for in打印打印不出來),可:enumerable: true
//默認不可以修改,可:wirtable:true
//默認不可以刪除,可:configurable:true
get: function () {
console.log('觸發了get方法')
return personName
},
set: function (val) {
console.log('觸發了set方法')
personName = val
}
})
//當讀取person對象的namp屬性時,觸發get方法
console.log(person.namep)
//當修改personName時,重新訪問person.namep發現修改成功
personName = 'liming'
console.log(person.namep)
// 對person.namep進行修改,觸發set方法
person.namep = 'huahua'
console.log(person.namep)
\
通過這種方法,我們成功監聽了 person 上的 name 屬性的變化。
- 監聽對象上的多個屬性
上面的使用中,我們只監聽了一個屬性的變化,但是在實際情況中,我們通常需要一次監聽多個屬性的變化。
這時我們需要配合 Object.keys(obj) 進行遍歷。這個方法可以返回 obj 對象身上的所有可枚舉屬性組成的字符數組。(其實用 for in 遍歷也可以)
下面是該 API 一個簡單的使用效果:
var obj = { 0: 'a', 1: 'b', 2: 'c' };
console.log(Object.keys(obj)); // console: ['0', '1', '2']
利用這個 API,我們就可以遍歷劫持對象的所有屬性 但是如果只是上面的思路與該 API 的簡單結合,我們就會發現並達不到效果,下面是我寫的一個錯誤的版本:
Object.keys(person).forEach(function (key) {
Object.defineProperty(person, key, {
enumerable: true,
configurable: true,
// 默認會傳入this
get() {
return person[key]
},
set(val) {
console.log(`對person中的${key}屬性進行了修改`)
person[key] = val
// 修改之後可以執行渲染操作
}
})
})
console.log(person.age)
看起來感覺上面的代碼沒有什麼錯誤,但是試着運行一下吧~ 你會和我一樣棧溢出。這是爲什麼呢?讓我們聚焦在 get 方法裏,我們在訪問 person 身上的屬性時,就會觸發 get 方法,返回 person[key],但是訪問 person[key] 也會觸發 get 方法,導致遞歸調用,最終棧溢出。
這也引出了我們下面的方法,我們需要設置一箇中轉 Obsever,來讓 get 中 return 的值並不是直接訪問 obj[key]。
let person = {
name: '',
age: 0
}
// 實現一個響應式函數
function defineProperty(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`訪問了${key}屬性`)
return val
},
set(newVal) {
console.log(`${key}屬性被修改爲${newVal}了`)
val = newVal
}
})
}
// 實現一個遍歷函數Observer
function Observer(obj) {
Object.keys(obj).forEach((key) => {
defineProperty(obj, key, obj[key])
})
}
Observer(person)
console.log(person.age)
person.age = 18
console.log(person.age)
- 深度監聽一個對象
那麼我們如何解決對象中嵌套一個對對象的情況呢?其實在上述代碼的基礎上,加上一個遞歸,就可以輕鬆實現啦~
我們可以觀察到,其實 Obsever 就是我們想要實現的監聽函數,我們預期的目標是: 只要把對象傳入其中,就可以實現對這個對象的屬性監視,即使該對象的屬性也是一個對象。
我們在 defineProperty() 函數中,添加一個遞歸的情況:
function defineProperty(obj, key, val) {
//如果某對象的屬性也是一個對象,遞歸進入該對象,進行監聽
if(typeof val === 'object'){
observer(val)
}
Object.defineProperty(obj, key, {
get() {
console.log(`訪問了${key}屬性`)
return val
},
set(newVal) {
console.log(`${key}屬性被修改爲${newVal}了`)
val = newVal
}
})
}
當然啦,我們也要在 observer 裏面加一個遞歸停止的條件:
function Observer(obj) {
//如果傳入的不是一個對象,return
if (typeof obj !== "object" || obj === null) {
return
}
// for (key in obj) {
Object.keys(obj).forEach((key) => {
defineProperty(obj, key, obj[key])
})
// }
}
其實到這裏就差不多解決了,但是還有一個小問題,如果對某屬性進行修改時,如果原本的屬性值是一個字符串,但是我們重新賦值了一個對象,我們要如何監聽新添加的對象的所有屬性呢?其實也很簡單,只需要修改 set 函數:
set(newVal) {
// 如果newVal是一個對象,遞歸進入該對象進行監聽
if(typeof val === 'object'){
observer(key)
}
console.log(`${key}屬性被修改爲${newVal}了`)
val = newVal
}
到這裏我們就完成啦~
- 監聽數組
那麼如果對象的屬性是一個數組呢?我們要如何實現監聽?請看下面一段代碼:
let arr = [1, 2, 3]
let obj = {}
//把arr作爲obj的屬性監聽
Object.defineProperty(obj, 'arr', {
get() {
console.log('get arr')
return arr
},
set(newVal) {
console.log('set', newVal)
arr = newVal
}
})
console.log(obj.arr)//輸出get arr [1,2,3] 正常
obj.arr = [1, 2, 3, 4] //輸出set [1,2,3,4] 正常
obj.arr.push(3) //輸出get arr 不正常,監聽不到push
我們發現,通過push
方法給數組增加的元素,set 方法是監聽不到的。
事實上,通過索引訪問或者修改數組中已經存在的元素,是可以出發 get 和 set 的,但是對於通過 push、unshift 增加的元素,會增加一個索引,這種情況需要手動初始化,新增加的元素才能被監聽到。另外,通過 pop 或 shift 刪除元素,會刪除並更新索引,也會觸發 setter 和 getter 方法。
在 Vue2.x 中,通過重寫 Array 原型上的方法解決了這個問題,此處就不展開說了,有興趣的 uu 可以再去了解下~
Proxy
是不是感覺有點複雜?事實上,在上面的講述中,我們還有問題沒有解決:那就是當我們要給對象新增加一個屬性時,也需要手動去監聽這個新增屬性。
也正是因爲這個原因,使用 vue 給 data 中的數組或對象新增屬性時,需要使用 vm.$set 才能保證新增的屬性也是響應式的。
可以看到,通過 Object.definePorperty() 進行數據監聽是比較麻煩的,需要大量的手動處理。這也是爲什麼在 Vue3.0 中尤雨溪轉而採用 Proxy。接下來讓我們一起看一下 Proxy 是怎麼解決這些問題的吧~
- 基本使用
語法:const p = new Proxy(target, handler)
參數:
-
target: 要使用
Proxy
包裝的目標對象(可以是任何類型的對象,包括原生數組,函數,甚至另一個代理) -
handler: 一個通常以函數作爲屬性的對象,各屬性中的函數分別定義了在執行各種操作時代理
p
的行爲。
通過 Proxy,我們可以對設置代理的對象
上的一些操作進行攔截,外界對這個對象的各種操作,都要先通過這層攔截。(和 defineProperty 差不多)
先看一個簡單例子
//定義一個需要代理的對象
let person = {
age: 0,
school: '西電'
}
//定義handler對象
let hander = {
get(obj, key) {
// 如果對象裏有這個屬性,就返回屬性值,如果沒有,就返回默認值66
return key in obj ? obj[key] : 66
},
set(obj, key, val) {
obj[key] = val
return true
}
}
//把handler對象傳入Proxy
let proxyObj = new Proxy(person, hander)
// 測試get能否攔截成功
console.log(proxyObj.age)//輸出0
console.log(proxyObj.school)//輸出西電
console.log(proxyObj.name)//輸出默認值66
// 測試set能否攔截成功
proxyObj.age = 18
console.log(proxyObj.age)//輸出18 修改成功
可以看出,Proxy 代理的是整個對象,而不是對象的某個特定屬性,不需要我們通過遍歷來逐個進行數據綁定。
值得注意的是: 之前我們在使用 Object.defineProperty() 給對象添加一個屬性之後,我們對對象屬性的讀寫操作仍然在對象本身。
但是一旦使用 Proxy,如果想要讀寫操作生效,我們就要對 Proxy 的實例對象proxyObj
進行操作。
另外,MDN 上明確指出 set() 方法應該返回一個布爾值,否則會報錯
TypeError
。
- 輕鬆解決 Object.defineProperty 中遇到的問題
在上面使用 Object.defineProperty 的時候,我們遇到的問題有:
- 一次只能對一個屬性進行監聽,需要遍歷來對所有屬性監聽。這個我們在上面已經解決了。
- 在遇到一個對象的屬性還是一個對象的情況下,需要遞歸監聽。
- 對於對象的新增屬性,需要手動監聽
- 對於數組通過 push、unshift 方法增加的元素,也無法監聽
這些問題在 Proxy 中都輕鬆得到了解決,讓我們看看以下代碼。
檢驗第二個問題
在上面代碼的基礎上,我們讓對象的結構變得更復雜一些。
let person = {
age: 0,
school: '西電',
children: {
name: '小明'
}
}
let hander = {
get(obj, key) {
return key in obj ? obj[key] : 66
}, set(obj, key, val) {
obj[key] = val
return true
}
}
let proxyObj = new Proxy(person, hander)
// 測試get
console.log(proxyObj.children.name)//輸出:小明
console.log(proxyObj.children.height)//輸出:undefined
// 測試set
proxyObj.children.name = '菜菜'
console.log(proxyObj.children.name)//輸出: 菜菜
可以看到成功監聽到了 children 對象身上的 name 屬性(至於爲什麼 children.height 是 undefined,可以再討論一下)
檢驗第三個問題
這個其實在基本使用裏面已經提到了,訪問的 proxyObj.name 就是原本對象上不存在的屬性,但是我們訪問它的時候,仍然們可以被 get 攔截到。
檢驗第四個問題
let subject = ['高數']
let handler = {
get(obj, key) {
return key in obj ? obj[key] : '沒有這門學科'
}, set(obj, key, val) {
obj[key] = val
//set方法成功時應該返回true,否則會報錯
return true
}
}
let proxyObj = new Proxy(subject, handler)
// 檢驗get和set
console.log(proxyObj)//輸出 [ '高數' ]
console.log(proxyObj[1])//輸出 沒有這門學科
proxyObj[0] = '大學物理'
console.log(proxyObj)//輸出 [ '大學物理' ]
// // 檢驗push增加的元素能否被監聽
proxyObj.push('線性代數')
console.log(proxyObj)//輸出 [ '大學物理', '線性代數' ]
至此,我們之前的問題完美解決。
3.Proxy 支持 13 種攔截操作
除了 get 和 set 來攔截讀取和賦值操作之外,Proxy 還支持對其他多種行爲的攔截。下面是一個簡單介紹,想要深入瞭解的可以去 MDN 上看看。
-
get(target, propKey, receiver):攔截對象屬性的讀取,比如 proxy.foo 和 proxy['foo']。
-
set(target, propKey, value, receiver):攔截對象屬性的設置,比如 proxy.foo = v 或 proxy['foo'] = v,返回一個布爾值。
-
has(target, propKey):攔截 propKey in proxy 的操作,返回一個布爾值。
-
deleteProperty(target, propKey):攔截 delete proxy[propKey] 的操作,返回一個布爾值。
-
ownKeys(target):攔截 Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in 循環,返回一個數組。該方法返回目標對象所有自身的屬性的屬性名,而 Object.keys() 的返回結果僅包括目標對象自身的可遍歷屬性。
-
getOwnPropertyDescriptor(target, propKey):攔截 Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述對象。
-
defineProperty(target, propKey, propDesc):攔截 Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一個布爾值。
-
preventExtensions(target):攔截 Object.preventExtensions(proxy),返回一個布爾值。
-
getPrototypeOf(target):攔截 Object.getPrototypeOf(proxy),返回一個對象。
-
isExtensible(target):攔截 Object.isExtensible(proxy),返回一個布爾值。
-
setPrototypeOf(target, proto):攔截 Object.setPrototypeOf(proxy, proto),返回一個布爾值。如果目標對象是函數,那麼還有兩種額外操作可以攔截。
-
apply(target, object, args):攔截 Proxy 實例作爲函數調用的操作,比如 proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
-
construct(target, args):攔截 Proxy 實例作爲構造函數調用的操作,比如 new proxy(...args)。
- Proxy 中有關 this 的問題
雖然 Proxy 完成了對目標對象的代理,但是它不是透明代理
, 也就是說:即使 handler 爲空對象(即不做任何代理),他所代理的對象中的 this 指向也不是該對象,而是 proxyObj 對象。讓我們來看一個例子:
let target = {
m() {
// 檢查this的指向是不是proxyObkj
console.log(this === proxyObj)
}
}
let handler = {}
let proxyObj = new Proxy(target, handler)
proxyObj.m()//輸出:true
target.m()//輸出:false
可以看到,被代理的對象 target 內部的 this 指向了 proxyObj。這種指向有時候就會導致問題出現,我們來看看下面一個例子:
const _name = new WeakMap();
class Person {
//把person的name存儲到_name的name屬性上
constructor(name) {
_name.set(this, name);
}
//獲取person的name屬性時,返回_name的name
get name() {
return _name.get(this);
}
}
const jane = new Person('Jane');
jane.name // 'Jane'
const proxyObj = new Proxy(jane, {});
proxyObj.name // undefined
在上面的例子中,由於 jane 對象的 name 屬性的獲取依靠 this 的指向,而 this 又指向 proxyObj,所以導致了無法正常代理。
除此之外,有的 js 內置對象的內部屬性,也依靠正確的 this 才能獲取,所以 Proxy 也無法代理這些原生對象的屬性。請看下面一個例子:
const target = new Date();
const handler = {};
const proxyObj = new Proxy(target, handler);
proxyObj.getDate();
// TypeError: this is not a Date object.
可以看到,通過 proxy 代理訪問 Date 對象中的 getDate 方法時拋出了一個錯誤,這是因爲 getDate 方法只能在 Date 對象實例上面拿到,如果 this 不是 Date 對象實例就會報錯。那麼我們要如何解決這個問題呢?只要手動把 this 綁定在 Date 對象實例上即可,請看下面一個例子:
const target = new Date('2015-01-01');
const handler = {
get(target, prop) {
if (prop === 'getDate') {
return target.getDate.bind(target);
}
return Reflect.get(target, prop);
}
};
const proxy = new Proxy(target, handler);
proxy.getDate() // 1
完結撒花
至此,我的總結就結束啦~ 文章不是很全面,還有很多地方沒有講到, 比如:
-
Proxy 常常搭配
Reflect
使用 -
我們常
Object.create()
方法把 Proxy 實例對象添加到 Object 的原型對象上,這樣我們就可以直接 Object.proxyObj 了 -
有興趣的 uu 可以在 Proxy 的 get 和 set 里加上輸出試試,你會發現在我們調用 push 方法時,
get和set會分別輸出兩次
,這是爲什麼呢?
學無止境,讓我們一起努力叭~
參考文章:
1.Proxy 與 Object.defineProperty 介紹與對比
2.MDN Proxy
Web 開發 分享 Web 後端開發技術,分享 PHP、Ruby、Python 等用於後端網站、後臺系統等後端開發技術;還包含 ThinkPHP,WordPress 等 PHP 網站開發框架、Django,Flask 等 Python 網站開發框架。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/bAM7d57Mmn7qP4GSpQ84Ow