最近在複習Vue,不可避免的會接觸到vue3,所以也不可避免的會思考這些問題
- vue3實現響應式為什麼要使用proxy替換Object.defineProperty?Proxy對比Object.defineProperty有啥優缺點?
- 怎麼通過Proxy實現響應式?
本文會回答這兩個問題,通過這些問題探討Proxy,以及Proxy在日常開發中的應用場景。
認識Proxy
Proxy意思翻譯過來就是代理,外界對目標物件的訪問都會被Proxy攔截,從而可以實現基本操作的攔截和自定義。
用法
let proxy = new Proxy(target,handler)
- target: 所要攔截的目標物件
- handler: handler是一個包含你要攔截和處理的物件,當物件被代理時,handler通過捕捉器(trap)實現對各種行為的攔截
目前proxy支援13種行為的攔截
handler方法 | 何時觸發 |
---|---|
get | 讀取屬性 |
set | 寫入屬性 |
has | in操作符 |
deleteProperty | delete操作符 |
apply | 函式呼叫 |
construct | new操作符 |
getPrototypeOf | Object.getPrototypeOf |
setPrototypeOf | Object.setPrototypeOf |
isExtensible | Object.isExtensible |
preventExtensions | Object.preventExtensions |
defineProperty | Object.defineProperty, Object.defineProperties |
getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor, for...in, Object.keys/values/entries |
ownKeys | Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for...in, Object.keys/values/entries |
Reflect
reflect翻譯過來是對映的意思,在MDN上是這樣定義的
Reflect 是一個內建的物件,它提供攔截 JavaScript 操作的方法。
每個可用的代理捕捉器(trap)都有一個對應的同名Reflect函式,並能產生相同的行為。
let obj = {
a: 10,
name: 'oyc'
}
let newTarget = new Proxy(obj, {
set(target, key, val) {
console.log(`Set ${key}=${val} `);
}
})
// newTarget.a = 20; //Set a=20
// Reflect.set(newTarget, 'a', 20); //Set a=20
newTarget.name = 'oyq'; //Set name=oyq
Reflect.set(newTarget, 'name', 'oyq'); //Set name=oyq
從這可以看出,Reflect和trap表現出來的行為是相同的。所以當你為如何去觸發trap而煩惱的時候,也許這個Reflect可以幫到你。
兩個問題
大致學習完proxy的內容後,再來嘗試解答下頁頭提到的兩個個問題。
vue3實現響應式為什麼要使用proxy替換Object.defineProperty?優缺點?
優點
- 效能更好,Object.defineProperty只能劫持物件的屬性,所以如果有巢狀物件,初始化時需要遍歷data中的每個屬性,在vue3中,proxy可以代理物件,不需要像vue2那樣對屬性進行遍歷的操作
//vue2
function reactive(obj) {
// 遍歷物件
for (const item in obj) {
if (obj[item] && typeof obj[item] == 'object') {
// 遞迴,又重新遍歷
reactive(obj[item])
} else {
defineReactive(obj, item, obj[item])
}
}
}
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
//set,get操作
})
}
//vue3
let newTarget = new Proxy(obj, {
// set,get操作
})
- 自動代理新增屬性,陣列,Object.defineProperty的實現是對屬性進行劫持,所以當新增屬性時,需要重新遍歷,對新增的重新進行劫持。所以需要vue2對新增的屬性,以及陣列進行 $set 才能保證屬性是響應式的,這個過程是手動的。
let obj = {
a: 10,
name: 'oyc'
}
//vue2
this.$set(this.obj, 'age', 18); //每次新增都需要進行這個操作
//vue3
//自動代理
let newTarget = new Proxy(obj, {
get(target, key) {
return Reflect.get(target, key);
},
set(target, key, val) {
return Reflect.set(target, key, val);
}
})
- Proxy支援13中攔截操作,Object.defineProperty無法比擬
- Proxy是新標準,後續也會優先優化,Object.defineProperty的setter,getter後續應該優化的優先順序較低
缺點
顯而易見的,Proxy的相容性相較於Object.defineProperty,是較低的,不支援IE瀏覽器。不過以目前的市場份額來看,IE瀏覽器的市場份額也不多,目前微軟也將ie換成了chrome核心的edge,所以激進點的專案是完全可以使用proxy的。
怎麼通過Proxy實現響應式?
let obj1 = {
a: 10,
name: 'John',
list: [1, 2, 3],
obj2: {
obj3: 'oo',
obj4: {
name: 'oyc'
}
}
}
// 判斷是否是物件
const isObj = (obj) => typeof obj === 'object' && obj !== null;
const render = (key, val) => {
console.log(`Render ${key}=${val}`);
}
function reactive(obj) {
if (!isObj(obj)) {
return obj;
}
const handler = {
get(target, key) {
// 對巢狀物件遍歷
if (isObj(target[key])) {
// 遞迴
return reactive(target[key]);
}
return Reflect.get(target, key);
},
set(target, key, val) {
// 渲染
render(key, val);
return Reflect.set(target, key, val);
}
}
const targetProxyObj = new Proxy(obj, handler);
return targetProxyObj
}
let myObj = reactive(obj1);
myObj.a = 20; // Render a=20
myObj.b = 30; //新增屬性 Render b=30
myObj.list = [1, 2, 5, 6]; //修改陣列 //Render list=1,2,5,6
myObj.obj2.obj4.name = 'oyq'; //修改巢狀物件 //Render name=oyq
Proxy應用場景
- 寫入預設值,日常開發經常碰到 ReferenceError: xxx is not defined 這種錯誤,這裡我們可以代理,當屬性不存在時,不報錯,而設定一個預設值
let obj = {
name: 'oyq'
}
let proxyObj = new Proxy(obj, {
get(target, key) {
if (Reflect.has(target, key)) {
return target[key];
} else {
return 'OYC';
}
},
})
console.log(proxyObj.age);//OYC
- 用Proxy來包裝fetch,讓fetch更易用
let handlers = {
get (target, property) {
if (!target.init) {
// 初始化物件
['GET', 'POST'].forEach(method => {
target[method] = (url, params = {}) => {
return fetch(url, {
headers: {
'content-type': 'application/json'
},
mode: 'cors',
credentials: 'same-origin',
method,
...params
}).then(response => response.json())
}
})
}
return target[property]
}
}
let API = new Proxy({}, handlers)
await API.GET('XXX')
await API.POST('XXX', {
body: JSON.stringify({name: 1})
})
- 檢驗表單
let formData = {
name: '',
age: ''
}
let proxyObj = new Proxy(formData, {
set(target, key, val) {
if (key === 'age' && typeof val !== 'number') {
console.log('age must be number');
}
if (key === 'name' && typeof val !== 'string') {
console.log('name must be string');
}
}
})
proxyObj.age = 'oyc'; //age must be number
proxyObj.name = 18; //name must be string
- 負索引陣列
let arr = [1, 2, 3, 4, 5]
let proxyArray = new Proxy(arr, {
get(target, key) {
const index = key < 0 ? target.length + Number(key) : key;
return Reflect.get(target, index)
}
})
console.log(proxyArray[-1]); //5