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