產(chǎn)品經(jīng)理身旁過,需求變更逃不過。 測試姐姐瞇眼笑,今晚bug必然多。
據(jù)悉Vue3.0
的正式版將要在本月(8月)發(fā)布,從發(fā)布到正式投入到正式項目中,還需要一定的過渡期,但我們不能一直等到Vue3
正式投入到項目中的時候才去學(xué)習(xí),提前學(xué)習(xí),讓你更快一步掌握Vue3.0
,升職加薪迎娶白富美就靠它了。不過在學(xué)習(xí)Vue3
之前,還需要先了解一下Proxy
,它是Vue3.0
實現(xiàn)數(shù)據(jù)雙向綁定的基礎(chǔ)。
了解代理模式
一個例子
作為一個單身鋼鐵直男程序員,小王最近逐漸喜歡上了前端小妹,不過呢,他又和前臺小妹不熟,所以決定委托與前端小妹比較熟的UI
小姐姐幫忙給自己搭橋引線。小王于是請UI
小姐姐吃了一頓大餐,然后拿出一封情書委托它轉(zhuǎn)交給前臺小妹,情書上寫的 我喜歡你,我想和你睡覺
,不愧鋼鐵直男。不過這樣寫肯定是沒戲的,UI
小姐姐吃人嘴短,于是幫忙改了情書,改成了我喜歡你,我想和你一起在晨輝的沐浴下起床
,然后交給了前臺小妹。雖然有沒有撮合成功不清楚啊,不過這個故事告訴我們,小王活該單身狗。
其實上面就是一個比較典型的代理模式的例子,小王想給前臺小妹送情書,因為不熟所以委托UI小姐姐
,UI
小姐姐相當(dāng)于代理人,代替小王完成了送情書的事情。
引申
通過上面的例子,我們想想Vue
的數(shù)據(jù)響應(yīng)原理,比如下面這段代碼
const xiaowang = {
love: '我喜歡你,我想和你睡覺'
}
// 送給小姐姐情書
function sendToMyLove(obj) {
console.log(obj.love)
return '流氓,滾'
}
console.log(sendToMyLove(xiaowang))
如果沒有UI
小姐姐代替送情書,顯示結(jié)局是悲慘的,想想Vue2.0
的雙向綁定,通過Object.defineProperty
來監(jiān)聽的屬性 get
,set
方法來實現(xiàn)雙向綁定,這個Object.defineProperty
就相當(dāng)于UI
小姐姐
const xiaowang = {
loveLetter: '我喜歡你,我想和你睡覺'
}
// UI小姐姐代理
Object.defineProperty(xiaowang,'love', {
get() {
return xiaowang.loveLetter.replace('睡覺','一起在晨輝的沐浴下起床')
}
})
// 送給小姐姐情書
function sendToMyLove(obj) {
console.log(obj.love)
return '小伙子還挺有詩情畫意的么,不過老娘不喜歡,滾'
}
console.log(sendToMyLove(xiaowang))
雖然依然是一個悲慘的故事,因為送奔馳的成功率可能會更高一些。但是我們可以看到,通過Object.defineproperty
可以對對象的已有屬性進行攔截,然后做一些額外的操作。
存在的問題
在Vue2.0
中,數(shù)據(jù)雙向綁定就是通過Object.defineProperty
去監(jiān)聽對象的每一個屬性,然后在get
,set
方法中通過發(fā)布訂閱者模式來實現(xiàn)的數(shù)據(jù)響應(yīng),但是存在一定的缺陷,比如只能監(jiān)聽已存在的屬性,對于新增刪除屬性就無能為力了,同時無法監(jiān)聽數(shù)組的變化,所以在Vue3.0
中將其換成了功能更強大的Proxy
。
(推薦教程:Vue 2教程)
了解Proxy
Proxy
是ES6
新推出的一個特性,可以用它去攔截js
操作的方法,從而對這些方法進行代理操作。
用Proxy重寫上面的例子
比如我們可以通過Proxy
對上面的送情書情節(jié)進行重寫:
const xiaowang = {
loveLetter: '我喜歡你,我想和你睡覺'
}
const proxy = new Proxy(xiaowang, {
get(target,key) {
if(key === 'loveLetter') {
return target[key].replace('睡覺','一起在晨輝的沐浴下起床')
}
}
})
// 送給小姐姐情書
function sendToMyLove(obj) {
console.log(obj.loveLetter)
return '小伙子還挺有詩情畫意的么,不過老娘不喜歡,滾'
}
console.log(sendToMyLove(proxy))
再看這樣一個場景
請分別使用Object.defineProperty
和Proxy
完善下面的代碼邏輯.
function observe(obj, callback) {}
const obj = observe(
{
name: '子君',
sex: '男'
},
(key, value) => {
console.log(`屬性[${key}]的值被修改為[${value}]`)
}
)
// 這段代碼執(zhí)行后,輸出 屬性[name]的值被修改為[妹紙]
obj.name = '妹紙'
// 這段代碼執(zhí)行后,輸出 屬性[sex]的值被修改為[女]
obj.name = '女'
看了上面的代碼,希望大家可以先自行實現(xiàn)以下,下面我們分別用Object.defineProperty
和Proxy
去實現(xiàn)上面的邏輯.
- 使用
Object.defineProperty
/**
* 請實現(xiàn)這個函數(shù),使下面的代碼邏輯正常運行
* @param {*} obj 對象
* @param {*} callback 回調(diào)函數(shù)
*/
function observe(obj, callback) {
const newObj = {}
Object.keys(obj).forEach(key => {
Object.defineProperty(newObj, key, {
configurable: true,
enumerable: true,
get() {
return obj[key]
},
// 當(dāng)屬性的值被修改時,會調(diào)用set,這時候就可以在set里面調(diào)用回調(diào)函數(shù)
set(newVal) {
obj[key] = newVal
callback(key, newVal)
}
})
})
return newObj
}
const obj = observe(
{
name: '子君',
sex: '男'
},
(key, value) => {
console.log(`屬性[${key}]的值被修改為[${value}]`)
}
)
// 這段代碼執(zhí)行后,輸出 屬性[name]的值被修改為[妹紙]
obj.name = '妹紙'
// 這段代碼執(zhí)行后,輸出 屬性[sex]的值被修改為[女]
obj.name = '女'
- 使用
Proxy
function observe(obj, callback) {
return new Proxy(obj, {
get(target, key) {
return target[key]
},
set(target, key, value) {
target[key] = value
callback(key, value)
}
})
}
const obj = observe(
{
name: '子君',
sex: '男'
},
(key, value) => {
console.log(`屬性[${key}]的值被修改為[${value}]`)
}
)
// 這段代碼執(zhí)行后,輸出 屬性[name]的值被修改為[妹紙]
obj.name = '妹紙'
// 這段代碼執(zhí)行后,輸出 屬性[sex]的值被修改為[女]
obj.name = '女'
通過上面兩種不同實現(xiàn)方式,我們可以大概的了解到Object.defineProperty
和Proxy
的用法,但是當(dāng)給對象添加新的屬性的時候,區(qū)別就出來了,比如
// 添加編程網(wǎng)站
obj.gzh = 'W3Cschool編程獅'
使用Object.defineProperty
無法監(jiān)聽到新增屬性,但是使用Proxy
是可以監(jiān)聽到的。對比上面兩段代碼可以發(fā)現(xiàn)有以下幾點不同
Object.defineProperty
監(jiān)聽的是對象的每一個屬性,而Proxy
監(jiān)聽的是對象自身- 使用
Object.defineProperty
需要遍歷對象的每一個屬性,對于性能會有一定的影響 Proxy
對新增的屬性也能監(jiān)聽到,但Object.defineProperty
無法監(jiān)聽到。
初識Proxy
概念與語法
在MDN
中,關(guān)于Proxy
是這樣介紹的: Proxy
對象用于定義基本操作的自定義行為(如屬性查找、賦值、枚舉、函數(shù)調(diào)用等)。什么意思呢?Proxy
就像一個攔截器一樣,它可以在讀取對象的屬性,修改對象的屬性,獲取對象屬性列表,通過for in
循環(huán)等等操作的時候,去攔截對象上面的默認(rèn)行為,然后自己去自定義這些行為,比如上面例子中的set
,我們通過攔截默認(rèn)的set
,然后在自定義的set
里面添加了回調(diào)函數(shù)的調(diào)用
Proxy
的語法格式如下
/**
* target: 要兼容的對象,可以是一個對象,數(shù)組,函數(shù)等等
* handler: 是一個對象,里面包含了可以監(jiān)聽這個對象的行為函數(shù),比如上面例子里面的`get`與`set`
* 同時會返回一個新的對象proxy, 為了能夠觸發(fā)handler里面的函數(shù),必須要使用返回值去進行其他操作,比如修改值
*/
const proxy = new Proxy(target, handler)
在上面的例子里面,我們已經(jīng)使用到了handler
里面提供的get
與set
方法了,接下來我們一一看一下handler
里面的方法。
handler 里面的方法列表
handler
里面的方法可以有以下這十三個,每一個都對應(yīng)的一種或多種針對proxy
代理對象的操作行為
handler.get
當(dāng)通過proxy
去讀取對象里面的屬性的時候,會進入到get
鉤子函數(shù)里面
handler.set
當(dāng)通過proxy
去為對象設(shè)置修改屬性的時候,會進入到set
鉤子函數(shù)里面
handler.has
當(dāng)使用in
判斷屬性是否在proxy
代理對象里面時,會觸發(fā)has
,比如
const obj = {
name: '子君'
}
console.log('name' in obj)
handler.deleteProperty
當(dāng)使用delete
去刪除對象里面的屬性的時候,會進入deleteProperty`鉤子函數(shù)
handler.apply
當(dāng)proxy
監(jiān)聽的是一個函數(shù)的時候,當(dāng)調(diào)用這個函數(shù)時,會進入apply
鉤子函數(shù)
handle.ownKeys
當(dāng)通過Object.getOwnPropertyNames
,Object.getownPropertySymbols
,Object.keys
,Reflect.ownKeys
去獲取對象的信息的時候,就會進入ownKeys
這個鉤子函數(shù)
handler.construct
當(dāng)使用new
操作符的時候,會進入construct
這個鉤子函數(shù)
handler.defineProperty
當(dāng)使用Object.defineProperty
去修改屬性修飾符的時候,會進入這個鉤子函數(shù)
handler.getPrototypeOf
當(dāng)讀取對象的原型的時候,會進入這個鉤子函數(shù)
handler.setPrototypeOf
當(dāng)設(shè)置對象的原型的時候,會進入這個鉤子函數(shù)
handler.isExtensible
當(dāng)通過Object.isExtensible
去判斷對象是否可以添加新的屬性的時候,進入這個鉤子函數(shù)
handler.preventExtensions
當(dāng)通過Object.preventExtensions
去設(shè)置對象不可以修改新屬性時候,進入這個鉤子函數(shù)
handler.getOwnPropertyDescriptor
在獲取代理對象某個屬性的屬性描述時觸發(fā)該操作,比如在執(zhí)行 Object.getOwnPropertyDescriptor(proxy, "foo")
時會進入這個鉤子函數(shù)
Proxy
提供了十三種攔截對象操作的方法,本文主要挑選其中一部分在Vue3
中比較重要的進行說明,其余的建議可以直接閱讀MDN
關(guān)于Proxy
的介紹。
詳細(xì)介紹
get
當(dāng)通過
proxy
去讀取對象里面的屬性的時候,會進入到get
鉤子函數(shù)里面
當(dāng)我們從一個proxy
代理上面讀取屬性的時候,就會觸發(fā)get
鉤子函數(shù),get
函數(shù)的結(jié)構(gòu)如下
/**
* target: 目標(biāo)對象,即通過proxy代理的對象
* key: 要訪問的屬性名稱
* receiver: receiver相當(dāng)于是我們要讀取的屬性的this,一般情況
* 下他就是proxy對象本身,關(guān)于receiver的作用,后文將具體講解
*/
handle.get(target,key, receiver)
示例
我們在工作中經(jīng)常會有封裝axios
的需求,在封裝過程中,也需要對請求異常進行封裝,比如不同的狀態(tài)碼返回的異常信息是不同的,如下是一部分狀態(tài)碼及其提示信息:
// 狀態(tài)碼提示信息
const errorMessage = {
400: '錯誤請求',
401: '系統(tǒng)未授權(quán),請重新登錄',
403: '拒絕訪問',
404: '請求失敗,未找到該資源'
}
// 使用方式
const code = 404
const message = errorMessage[code]
console.log(message)
但這存在一個問題,狀態(tài)碼很多,我們不可能每一個狀態(tài)碼都去枚舉出來,所以對于一些異常狀態(tài)碼,我們希望可以進行統(tǒng)一提示,如提示為系統(tǒng)異常,請聯(lián)系管理員
,這時候就可以使用Proxy
對錯誤信息進行代理處理
// 狀態(tài)碼提示信息
const errorMessage = {
400: '錯誤請求',
401: '系統(tǒng)未授權(quán),請重新登錄',
403: '拒絕訪問',
404: '請求失敗,未找到該資源'
}
const proxy = new Proxy(errorMessage, {
get(target,key) {
const value = target[key]
return value || '系統(tǒng)異常,請聯(lián)系管理員'
}
})
// 輸出 錯誤請求
console.log(proxy[400])
// 輸出 系統(tǒng)異常,請聯(lián)系管理員
console.log(proxy[500])
set
當(dāng)為對象里面的屬性賦值的時候,會觸發(fā)
set
當(dāng)給對象里面的屬性賦值的時候,會觸發(fā)set
,set
函數(shù)的結(jié)構(gòu)如下
/**
* target: 目標(biāo)對象,即通過proxy代理的對象
* key: 要賦值的屬性名稱
* value: 目標(biāo)屬性要賦的新值
* receiver: 與 get的receiver 基本一致
*/
handle.set(target,key,value, receiver)
示例
某系統(tǒng)需要錄入一系列數(shù)值用于數(shù)據(jù)統(tǒng)計,但是在錄入數(shù)值的時候,可能錄入的存在一部分異常值,對于這些異常值需要在錄入的時候進行處理, 比如大于100
的值,轉(zhuǎn)換為100
, 小于0
的值,轉(zhuǎn)換為0
, 這時候就可以使用proxy
的set
,在賦值的時候,對數(shù)據(jù)進行處理
const numbers = []
const proxy = new Proxy(numbers, {
set(target,key,value) {
if(value < 0) {
value = 0
}else if(value > 100) {
value = 100
}
target[key] = value
// 對于set 來說,如果操作成功必須返回true, 否則會被視為失敗
return true
}
})
proxy.push(1)
proxy.push(101)
proxy.push(-10)
// 輸出 [1, 100, 0]
console.log(numbers)
對比Vue2.0
在使用Vue2.0
的時候,如果給對象添加新屬性的時候,往往需要調(diào)用$set
, 這是因為Object.defineProperty
只能監(jiān)聽已存在的屬性,而新增的屬性無法監(jiān)聽,而通過$set
相當(dāng)于手動給對象新增了屬性,然后再觸發(fā)數(shù)據(jù)響應(yīng)。但是對于Vue3.0
來說,因為使用了Proxy
, 在他的set
鉤子函數(shù)中是可以監(jiān)聽到新增屬性的,所以就不再需要使用$set
const obj = {
name: '子君'
}
const proxy = new Proxy(obj, {
set(target,key,value) {
if(!target.hasOwnProperty(key)) {
console.log(`新增了屬性${key},值為${value}`)
}
target[key] = value
return true
}
})
// 新增 公眾號 屬性
// 輸出 新增了屬性gzh,值為前端有的玩
proxy.gzh = '前端有的玩'
has
當(dāng)使用
in
判斷屬性是否在proxy
代理對象里面時,會觸發(fā)has
/**
* target: 目標(biāo)對象,即通過proxy代理的對象
* key: 要判斷的key是否在target中
*/
handle.has(target,key)
示例
一般情況下我們在js
中聲明私有屬性的時候,會將屬性的名字以_
開頭,對于這些私有屬性,是不需要外部調(diào)用,所以如果可以隱藏掉是最好的,這時候就可以通過has
在判斷某個屬性是否在對象時,如果以_
開頭,則返回false
const obj = {
publicMethod() {},
_privateMethod(){}
}
const proxy = new Proxy(obj, {
has(target, key) {
if(key.startsWith('_')) {
return false
}
return Reflect.get(target,key)
}
})
// 輸出 false
console.log('_privateMethod' in proxy)
// 輸出 true
console.log('publicMethod' in proxy)
deleteProperty
當(dāng)使用
delete
去刪除對象里面的屬性的時候,會進入deleteProperty`攔截器
/**
* target: 目標(biāo)對象,即通過proxy代理的對象
* key: 要刪除的屬性
*/
handle.deleteProperty(target,key)
示例
現(xiàn)在有一個用戶信息的對象,對于某些用戶信息,只允許查看,但不能刪除或者修改,對此使用Proxy
可以對不能刪除或者修改的屬性進行攔截并拋出異常,如下
const userInfo = {
name: '子君',
gzh: '前端有的玩',
sex: '男',
age: 22
}
// 只能刪除用戶名和公眾號
const readonlyKeys = ['name', 'gzh']
const proxy = new Proxy(userInfo, {
set(target,key,value) {
if(readonlyKeys.includes(key)) {
throw new Error(`屬性${key}不能被修改`)
}
target[key] = value
return true
},
deleteProperty(target,key) {
if(readonlyKeys.includes(key)) {
throw new Error(`屬性${key}不能被刪除`)
return
}
delete target[key]
return true
}
})
// 報錯
delete proxy.name
對比Vue2.0
其實與$set
解決的問題類似,Vue2.0
是無法監(jiān)聽到屬性被刪除的,所以提供了$delete
用于刪除屬性,但是對于Proxy
,是可以監(jiān)聽刪除操作的,所以就不需要再使用$delete
了
其他操作
在上文中,我們提到了Proxy
的handler
提供了十三個函數(shù),在上面我們列舉了最常用的三個,其實每一個的用法都是基本一致的,比如ownKeys
,當(dāng)通過Object.getOwnPropertyNames
,Object.getownPropertySymbols
,Object.keys
,Reflect.ownKeys
去獲取對象的信息的時候,就會進入ownKeys
這個鉤子函數(shù),使用這個我們就可以對一些我們不像暴露的屬性進行保護,比如一般會約定_
開頭的為私有屬性,所以在使用Object.keys
去獲取對象的所有key
的時候,就可以把所有_
開頭的屬性屏蔽掉。關(guān)于剩余的那些屬性,建議大家多去看看MDN
中的介紹。
Reflect
在上面,我們獲取屬性的值或者修改屬性的值都是通過直接操作target
來實現(xiàn)的,但實際上ES6
已經(jīng)為我們提供了在Proxy
內(nèi)部調(diào)用對象的默認(rèn)行為的API
,即Reflect
。比如下面的代碼
const obj = {}
const proxy = new Proxy(obj, {
get(target,key,receiver) {
return Reflect.get(target,key,receiver)
}
})
大家可能看到上面的代碼與直接使用target[key]
的方式?jīng)]什么區(qū)別,但實際上Reflect
的出現(xiàn)是為了讓Object
上面的操作更加規(guī)范,比如我們要判斷某一個prop
是否在一個對象中,通常會使用到in
,即
const obj = {name: '子君'}
console.log('name' in obj)
但上面的操作是一種命令式的語法,通過Reflect
可以將其轉(zhuǎn)變?yōu)楹瘮?shù)式的語法,顯得更加規(guī)范
Reflect.has(obj,'name')
除了has
,get
之外,其實Reflect
上面總共提供了十三個靜態(tài)方法,這十三個靜態(tài)方法與Proxy
的handler
上面的十三個方法是一一對應(yīng)的,通過將Proxy
與Reflect
相結(jié)合,就可以對對象上面的默認(rèn)操作進行攔截處理,當(dāng)然這也就屬于函數(shù)元編程的范疇了。
(推薦微課:Vue 2.x 微課)
總結(jié)
有的同學(xué)可能會有疑惑,我不會Proxy
和Reflect
就學(xué)不了Vue3.0
了嗎?其實懂不懂這個是不影響學(xué)習(xí)Vue3.0
的,但是如果想深入 去理解Vue3.0
,還是很有必要了解這些的。比如經(jīng)常會有人在使用Vue2
的時候問,為什么我數(shù)組通過索引修改值之后,界面沒有變呢?當(dāng)你了解到Object.defineProperty
的使用方式與限制之后,就會恍然大悟,原來如此。
來源公眾號:前端有點玩
文章來源鏈接:mp.weixin.qq.com/s/JQDA6bP805xuN-tzuimtAA
作者:前端進擊者
以上就是關(guān)于學(xué)習(xí)Vue3.0,你需要先了解一下Proxy的相關(guān)介紹了,希望對大家有所幫助。