產品經理身旁過,需求變更逃不過。
測試姐姐眯眼笑,今晚bug必然多。
據悉Vue3.0
的正式版將要在本月(8月)釋出,從釋出到正式投入到正式專案中,還需要一定的過渡期,但我們不能一直等到Vue3
正式投入到專案中的時候才去學習,提前學習,讓你更快一步掌握Vue3.0
,升職加薪迎娶白富美就靠它了。不過在學習Vue3
之前,還需要先了解一下Proxy
,它是Vue3.0
實現資料雙向繫結的基礎。
本文是作者關於Vue3.0系列的第一篇文章,後續作者將會每週釋出一篇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
來監聽的屬性 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
去監聽物件的每一個屬性,然後在get
,set
方法中通過釋出訂閱者模式來實現的資料響應,但是存在一定的缺陷,比如只能監聽已存在的屬性,對於新增刪除屬性就無能為力了,同時無法監聽陣列的變化,所以在Vue3.0
中將其換成了功能更強大的Proxy
。
瞭解Proxy
Proxy
是ES6
新推出的一個特性,可以用它去攔截js
操作的方法,從而對這些方法進行代理操作。
用Proxy重寫上面的例子
比如我們可以通過Proxy
對上面的送情書情節進行重寫:
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}]`)
}
)
// 這段程式碼執行後,輸出 屬性[name]的值被修改為[妹紙]
obj.name = '妹紙'
// 這段程式碼執行後,輸出 屬性[sex]的值被修改為[女]
obj.sex = '女'
看了上面的程式碼,希望大家可以先自行實現以下,下面我們分別用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}]`)
}
)
// 這段程式碼執行後,輸出 屬性[name]的值被修改為[妹紙]
obj.name = '妹紙'
// 這段程式碼執行後,輸出 屬性[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}]`)
}
)
// 這段程式碼執行後,輸出 屬性[name]的值被修改為[妹紙]
obj.name = '妹紙'
// 這段程式碼執行後,輸出 屬性[sex]的值被修改為[女]
obj.name = '女'
通過上面兩種不同實現方式,我們可以大概的瞭解到Object.defineProperty
和Proxy
的用法,但是當給物件新增新的屬性的時候,區別就出來了,比如
// 新增公眾號欄位
obj.gzh = '前端有的玩'
使用Object.defineProperty
無法監聽到新增屬性,但是使用Proxy
是可以監聽到的。對比上面兩段程式碼可以發現有以下幾點不同
Object.defineProperty
監聽的是物件的每一個屬性,而Proxy
監聽的是物件自身- 使用
Object.defineProperty
需要遍歷物件的每一個屬性,對於效能會有一定的影響 Proxy
對新增的屬性也能監聽到,但Object.defineProperty
無法監聽到。
初識Proxy
概念與語法
在MDN
中,關於Proxy
是這樣介紹的: Proxy
物件用於定義基本操作的自定義行為(如屬性查詢、賦值、列舉、函式呼叫等)。什麼意思呢?Proxy
就像一個攔截器一樣,它可以在讀取物件的屬性,修改物件的屬性,獲取物件屬性列表,通過for in
迴圈等等操作的時候,去攔截物件上面的預設行為,然後自己去自定義這些行為,比如上面例子中的set
,我們通過攔截預設的set
,然後在自定義的set
裡面新增了回撥函式的呼叫
Proxy
的語法格式如下
/**
* target: 要相容的物件,可以是一個物件,陣列,函式等等
* handler: 是一個物件,裡面包含了可以監聽這個物件的行為函式,比如上面例子裡面的`get`與`set`
* 同時會返回一個新的物件proxy, 為了能夠觸發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
代理物件裡面時,會觸發has
,比如const obj = { name: '子君' } console.log('name' in obj)
handler.deleteProperty
當使用
delete
去刪除物件裡面的屬性的時候,會進入deleteProperty`鉤子函式handler.apply
當
proxy
監聽的是一個函式的時候,當呼叫這個函式時,會進入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
在獲取代理物件某個屬性的屬性描述時觸發該操作,比如在執行
Object.getOwnPropertyDescriptor(proxy, "foo")
時會進入這個鉤子函式
Proxy
提供了十三種攔截物件操作的方法,本文主要挑選其中一部分在Vue3
中比較重要的進行說明,其餘的建議可以直接閱讀MDN
關於Proxy
的介紹。
詳細介紹
get
當通過proxy
去讀取物件裡面的屬性的時候,會進入到get
鉤子函式裡面
當我們從一個proxy
代理上面讀取屬性的時候,就會觸發get
鉤子函式,get
函式的結構如下
/**
* target: 目標物件,即通過proxy代理的物件
* key: 要訪問的屬性名稱
* receiver: receiver相當於是我們要讀取的屬性的this,一般情況
* 下他就是proxy物件本身,關於receiver的作用,後文將具體講解
*/
handle.get(target,key, receiver)
示例
我們在工作中經常會有封裝axios
的需求,在封裝過程中,也需要對請求異常進行封裝,比如不同的狀態碼返回的異常資訊是不同的,如下是一部分狀態碼及其提示資訊:
// 狀態碼提示資訊
const errorMessage = {
400: '錯誤請求',
401: '系統未授權,請重新登入',
403: '拒絕訪問',
404: '請求失敗,未找到該資源'
}
// 使用方式
const code = 404
const message = errorMessage[code]
console.log(message)
但這存在一個問題,狀態碼很多,我們不可能每一個狀態碼都去列舉出來,所以對於一些異常狀態碼,我們希望可以進行統一提示,如提示為系統異常,請聯絡管理員
,這時候就可以使用Proxy
對錯誤資訊進行代理處理
// 狀態碼提示資訊
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
當為物件裡面的屬性賦值的時候,會觸發set
當給物件裡面的屬性賦值的時候,會觸發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
只能監聽已存在的屬性,而新增的屬性無法監聽,而通過$set
相當於手動給物件新增了屬性,然後再觸發資料響應。但是對於Vue3.0
來說,因為使用了Proxy
, 在他的set
鉤子函式中是可以監聽到新增屬性的,所以就不再需要使用$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
代理物件裡面時,會觸發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
是無法監聽到屬性被刪除的,所以提供了$delete
用於刪除屬性,但是對於Proxy
,是可以監聽刪除操作的,所以就不需要再使用$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]
的方式沒什麼區別,但實際上Reflect
的出現是為了讓Object
上面的操作更加規範,比如我們要判斷某一個prop
是否在一個物件中,通常會使用到in
,即
const obj = {name: '子君'}
console.log('name' in obj)
但上面的操作是一種命令式的語法,通過Reflect
可以將其轉變為函式式的語法,顯得更加規範
Reflect.has(obj,'name')
除了has
,get
之外,其實Reflect
上面總共提供了十三個靜態方法,這十三個靜態方法與Proxy
的handler
上面的十三個方法是一一對應的,通過將Proxy
與Reflect
相結合,就可以對物件上面的預設操作進行攔截處理,當然這也就屬於函式超程式設計的範疇了。
總結
有的同學可能會有疑惑,我不會Proxy
和Reflect
就學不了Vue3.0
了嗎?其實懂不懂這個是不影響學習Vue3.0
的,但是如果想深入 去理解Vue3.0
,還是很有必要了解這些的。比如經常會有人在使用Vue2
的時候問,為什麼我陣列通過索引修改值之後,介面沒有變呢?當你瞭解到Object.defineProperty
的使用方式與限制之後,就會恍然大悟,原來如此。本文之後,小編將為大家帶來Vue3.0
系列文章,歡迎關注,一起學習。同時本文首發於公眾號【前端有的玩】,用玩的姿勢學前端,就在【前端有的玩】