引言
Vue中的數據綁定
Vue作為前端框架的三駕馬車之一,在眾多前端項目中具有極其重要的作用。
Vue中具有一個重要的功能點——“數據綁定”。使用者無需關心數據是如何綁定到dom上面,只需要關注數據本身即可。
那實現其功能的原理是什麼?
閱讀官方文檔(v2.0),我們會發現:
把一個普通 Javascript 對象傳給 Vue 實例來作為它的 data 選項,Vue 將遍歷它的屬性,用 Object.defineProperty 將它們轉為 getter/setter。
關鍵字是Object.defineProperty,在MDN文檔找到說明如下:
Object.defineProperty()方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。
我們再仔細查詢在MDN文檔的說明會發現,Object.defineProperty()存在兩種屬性描述符:
數據描述符(簡略介紹)
- configurable:數據可改變
- enumerable:可枚舉
- value:屬性值
- writable:可讀寫
存取描述符
- get:一個給屬性提供 getter 的方法,如果沒有 getter 則為 undefined。
- set:一個給屬性提供 setter 的方法,如果沒有 setter 則為 undefined。
至此也就引出了getter/setter。
ES5 getter/setter
讓我們通過一個例子來測試一下。
首先,建立一個英雄(Hero)對象並賦予其一些屬性:
<code>let hero = {
name:'趙雲',
hp: 100,
sp: 100
}
/<code>
然後使用Object.defineProperty()來對其具有的一些屬性進行修改,並且在控制檯輸出修改的內容:
<code>Object.defineProperty(hero, 'hp', {
set (val) {
console.log(`Set hp to ${val}`);
return val;
}
})
hero.hp = '200';
// --> Set hp to 200
/<code>
假若把console.log('Set hp to ${val}') 改為 element.innerHTML = val,是不是就可以實現數據綁定了?
那讓我們再修改一下英雄的屬性,假設英雄擁有很多裝備:
<code>let hero = {
name:'趙雲',
hp: 100,
sp: 100,
equipment:['馬','長槍']
}
/<code>
我們把“佩劍”添加到英雄的裝備中,並且輸出在控制檯:
<code>Object.defineProperty(hero.equipment, 'push', {
value () {
this[this.length] = arguments[0];
}
})
hero.equipment.push('佩劍');
console.log(hero.equipment);
// --> [ '馬','長槍', '佩劍' ]
/<code>
由此,我們可以看到對象的屬性變化可以依靠get()和set()方法去追蹤和改變;但對於數組則需要使用value()方法實現。
顯然,這不是最好的方法,那有沒有更好的方法可以簡化對象或數組屬性變化呢?
答案是肯定的。
概念
Proxy
Proxy意思為“代理”,即在訪問對象之前建立一道“攔截”,任何訪問該對象的操作之前都會通過這道“攔截”,即執行Proxy裡面定義的方法。
基本用法:
<code>let pro = new Proxy(target,handler);
/<code>
- new Proxy()表示生成一個Proxy實例
- target參數表示所要攔截的目標對象
- handler參數也是一個對象,用來定製攔截行為。
handler
Proxy支持13種攔截行為(handle),針對解決上一節的問題,簡單介紹下其中2種攔截行為,get與set。
get
get(target, propKey, receiver)
用於攔截某個屬性的讀取操作,可以接受三個參數:
- target:目標對象
- propKey:屬性名
- receiver(可選):proxy 實例本身(嚴格地說,是操作行為所針對的對象)
set
set(target, propKey, value, receiver)
用於攔截某個屬性的賦值操作,可以接受四個參數:
- target:目標對象
- propKey:屬性名
- value:屬性值
- receiver(可選):Proxy 實例本身
實例
在解決上一節問題之前,先一同看幾個實例。
實例1
<code>let hero = {
name: "趙雲",
age: 25
}
let handler = {}
let heroProxy = new Proxy(hero, handler);
console.log(heroProxy.name);
// --> 趙雲
heroProxy.name = "黃忠";
console.log(heroProxy.name);
// --> 黃忠
/<code>
解析:
創建hero對象為所要攔截的對象;
攔截操作對象handler為空,未對攔截對象設定攔截方法;
該情況下heroProxy直接指向原對象target,訪問heroProxy等同於訪問target,所以結果為target中的結果。
實例2
<code>let hero = {
name: "趙雲",
age: 25
}
let handler = {
get: (hero, name, ) => {
const heroName =`英雄名是${hero.name}`;
return heroName;
},
set:(hero,name,value)=>{
console.log(`${hero.name} change to ${value}`);
hero[name] = value;
return true;
}
}
let heroProxy = new Proxy(hero, handler);
console.log(heroProxy.name);
heroProxy.name = '黃忠';
console.log(heroProxy.name);
// --> 英雄名是趙雲
// --> 趙雲 change to 黃忠
// --> 英雄名是黃忠
/<code>
解析:
創建hero對象為所要攔截的對象;
handler對象為攔截對象後執行的操作,這裡get方法為讀取操作,即用戶想要讀取heroProxy中的屬性時執行的攔截操作。
最後創建一個Proxy實例,當讀取heroProxy中的屬性時,結果打印出來的總是“黃忠”字符串。
實例3
Proxy也可以作為其他對象的原型對象使用。
<code>let hero = {
name: "趙雲",
age: 25
}
let handler = {
get: (hero, name, ) => {
const heroName =`英雄名是${hero.name}`;
return heroName;
},
set:(hero,name,value)=>{
console.log(`${hero.name} change to ${value}`);
hero[name] = value;
return true;
}
}
let heroProxy = new Proxy(hero, handler);
let obj = Object.create(heroProxy);
console.log(obj.name);
obj.name = '黃忠';
console.log(obj.name);
// --> 英雄名是趙雲
// --> 趙雲 change to 黃忠
// --> 英雄名是黃忠
/<code>
解析:
在實例2的基礎上,將heroProxy作為obj的原型對象使用。
雖然obj本身沒有name這個屬性,但是根據原型鏈,會在heroProxy上讀取到name屬性,之後會執行相對應的攔截操作。
解決數據綁定問題
在我們對Proxy有了一定了解後,可以嘗試解決上一節的問題。
首先,還是定義一個英雄:
<code>let hero = {
name:'趙雲',
hp: 100,
sp: 100,
equipment:['馬','長槍']
}
/<code>
接著,定義一個handler:
<code>let handler = {
set(target, property, value) {
console.log(`hero's ${property} change to ${value}`);
target[property] = value;
return true;
}
}
/<code>
然後,修改英雄的hp值
<code>let heroProxy = new Proxy(hero, handler);
heroProxy.hp = 200;
// --> hero's hp change to 200
console.log(hero.hp);
// --> 200
/<code>
最後,同樣把“佩劍”添加到英雄的裝備中
<code>let heroProxy = new Proxy(hero.equipment, handler);
heroProxy.push('佩劍');
// --> hero's 2 change to 佩劍
// --> hero's length change to 3
console.log(hero.equipment);
// --> ["馬", "長槍", "佩劍"]
/<code>
可以發現,heroProxy.push('佩劍');觸發了兩次set,原因是push即修改了hero.equipment的內容,又修改了hero.equipment的length。
Reflect
在瞭解了Proxy之後,細心的我們一定發現,若需要在Proxy內部調用對象的默認行為,該如何實現?
Reflect正是ES6 為了操作對象而提供的新 API。
基本特點
- 只要Proxy對象具有的代理方法,Reflect對象全部具有,以靜態方法的形式存在。這些方法能夠執行默認行為,無論Proxy怎麼修改默認行為,總是可以通過Reflect對應的方法獲取默認行為。
- 修改某些Object方法的返回結果,讓其變得更合理。比如,Object.defineProperty(obj, name, desc)在無法定義屬性時,會拋出一個錯誤,而Reflect.defineProperty(obj, name, desc)則會返回false。
- 讓Object操作都變成函數行為。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)讓它們變成了函數行為。
靜態方法
Reflect對象一共有 13 個靜態方法(匹配Proxy的13種攔截行為)。
- Reflect.apply(target, thisArg, args)
- Reflect.construct(target, args)
- Reflect.get(target, name, receiver)
- Reflect.set(target, name, value, receiver)
- Reflect.defineProperty(target, name, desc)
- Reflect.deleteProperty(target, name)
- Reflect.has(target, name)
- Reflect.ownKeys(target)
- Reflect.isExtensible(target)
- Reflect.preventExtensions(target)
- Reflect.getOwnPropertyDescriptor(target, name)
- Reflect.getPrototypeOf(target)
- Reflect.setPrototypeOf(target, prototype)
大部分與Object對象的同名方法的作用都是相同的,而且它與Proxy對象的方法是一一對應的。
實例
下面通過3個實例來對比Object對象方法與Reflect對象方法。
實例1
<code>//Object對象方法
try {
Object.defineProperty(target, name, property);
} catch (e) {
console.log("error");
}
//Reflect對象方法
if (Reflect(target, name, property)) {
console.log("success");
} else {
console.log("error")
}
/<code>
解析:
由於Reflect(target, name, property)返回的是boolean,代碼語義性更好。
實例2
<code>let hero = {
name: '趙雲',
hp: 100,
sp: 100,
equipment: ['馬', '長槍']
}
//Object對象方法
console.log('name' in hero);
// --> true
//Reflect對象方法
console.log(Reflect.has(hero,'name'));
// --> true
/<code>
解析:
Object操作是命令式,而Reflect讓它們變成了函數行為
實例3
<code>let hero = {
name: '趙雲',
hp: 100,
sp: 100,
equipment: ['馬', '長槍']
}
let handler = {
get(target, name, receiver) {
if (name === "name") {
console.log("success");
} else {
console.log("failure");
}
return Reflect.get(target, name, receiver);
}
}
let heroProxy = new Proxy(hero, handler);
console.log(heroProxy.name);
// --> success
// --> 趙雲
/<code>
解析:
Reflect對象的操作和Proxy對象的操作一一對應,在Proxy的攔截操作中,可以直接利用Reflect對象直接獲取Proxy的默認值。
結合實踐
掌握了Proxy與Reflect的知識點後,除了解決文章開頭的數據綁定問題之外,挑選日常編碼中容易遇見的兩種情況進行編碼實踐。
觀察者模式
觀察者模式(Observer mode)指的是函數自動觀察數據對象,一旦數據有變化,函數就會自動執行。
<code>let hero = {
name: '趙雲',
hp: 100,
sp: 100,
equipment: ['馬', '長槍']
}
const handler = {
set(target, key, value, receiver) {
//內部調用對應的 Reflect 方法
const result = Reflect.set(target, key, value, receiver);
//執行觀察者隊列
observableArray.forEach(item => item());
return result;
}
}
//初始化Proxy對象,設置攔截操作
const createProxy = (obj) => new Proxy(obj, handler);
//初始化觀察者隊列
const observableArray = new Set();
const heroProxy = createProxy(hero);
//將監聽函數加入隊列
observableArray.add(() => {
console.log(heroProxy.name);
});
heroProxy.name = "黃忠";
// --> 黃忠
/<code>
該實例在set攔截行為中加入了監聽函數的執行,使每一次值的改變均能被監聽。
對象多重繼承
實現對象間的單繼承,比如obj2繼承obj1,可以使用Object.setPrototypeOf方法,但是沒法實現多繼承。
<code>const people = {
name: 'people',
run() {
console.log('people.run:', this.name);
}
};
const powerMan = {
name: 'powerMan',
run() {
console.log('powerMan.run:', this.name);
},
fight() {
console.log('powerMan.fight:', this.name);
}
};
const handler = {
get(target, name, receiver) {
if (Reflect.has(target, name)) {
return Reflect.get(target, name, receiver);
}
else {
for (let P of target[Symbol.for('[[Prototype]]')]) {
if (Reflect.has(P, name)) {
return Reflect.get(P, name, receiver);
}
}
}
}
};
const hero = new Proxy({
name: 'hero',
strike() {
this.run();
this.fight();
}
}, handler);
hero[Symbol.for('[[Prototype]]')] = [people, powerMan];
hero.strike();
// --> people.run:hero
// --> powerMan.fight:hero
/<code>
用了一個自定義的屬性Symbol.for("[[Prototype]]")來表示要繼承的多個父對象。
然後用Proxy來攔截所有hero中的get請求,使用Reflect.has方法檢查hero中是否存在相應的屬性或者方法。
如果存在,則直接轉發;如果不存在,則遍歷父對象列表,在父對象中逐個檢查是否存在相應的屬性或者方法。
若存在則調用。若不存在,則相當於get返回undefined。
如果文章能幫助你理解一些前端知識點,請收藏、分享和關注本專欄,謝謝。
閱讀更多 路演前端 的文章
關鍵字: JavaScript 趙雲 屬性