前言
"數(shù)據(jù)綁定" 的關(guān)鍵在于監(jiān)聽(tīng)數(shù)據(jù)的變化粟矿,vue數(shù)據(jù)雙向綁定是通過(guò)數(shù)據(jù)劫持結(jié)合發(fā)布者-訂閱者模式的方式來(lái)實(shí)現(xiàn)的。其實(shí)主要是用了ES5中的Object.defineProperty方法來(lái)劫持對(duì)象的屬性添加或修改的操作键袱,從而更新視圖侮穿。
聽(tīng)說(shuō)vue3.0 會(huì)用 proxy 替代 Object.defineProperty()方法薪伏。所以預(yù)先了解一些用法是有必要的播玖。proxy 能夠直接 劫持整個(gè)對(duì)象,而不是對(duì)象的屬性,并且劫持的方法有多種洛二。而且最后會(huì)返回劫持后的新對(duì)象。所以相對(duì)來(lái)講攻锰,這個(gè)方法還是挺好用的晾嘶。不過(guò)兼容性不太好。
一娶吞、defineProperty
ES5 提供了 Object.defineProperty 方法垒迂,該方法可以在一個(gè)對(duì)象上定義一個(gè)新屬性,或者修改一個(gè)對(duì)象的現(xiàn)有屬性妒蛇,并返回這個(gè)對(duì)象机断。
【1】語(yǔ)法
Object.defineProperty(obj, prop, descriptor)
參數(shù):
obj:必需,目標(biāo)對(duì)象
prop:必需绣夺,需定義或修改的屬性的名字
descriptor:必需吏奸,將被定義或修改的屬性的描述符
返回值:
傳入函數(shù)的對(duì)象,即第一個(gè)參數(shù)obj
【2】descriptor參數(shù)解析
函數(shù)的第三個(gè)參數(shù) descriptor 所表示的屬性描述符有兩種形式:數(shù)據(jù)描述符和存取描述符
數(shù)據(jù)描述:當(dāng)修改或定義對(duì)象的某個(gè)屬性的時(shí)候陶耍,給這個(gè)屬性添加一些特性奋蔚,數(shù)據(jù)描述中的屬性都是可選的
- value:屬性對(duì)應(yīng)的值,可以使任意類型的值,默認(rèn)為undefined
- writable:屬性的值是否可以被重寫。設(shè)置為true可以被重寫泊碑;設(shè)置為false坤按,不能被重寫。默認(rèn)為false
- enumerable:此屬性是否可以被枚舉(使用for...in或Object.keys())馒过。設(shè)置為true可以被枚舉臭脓;設(shè)置為false,不能被枚舉沉桌。默認(rèn)為false
- configurable:是否可以刪除目標(biāo)屬性或是否可以再次修改屬性的特性(writable, configurable, enumerable)谢鹊。設(shè)置為true可以被刪除或可以重新設(shè)置特性;設(shè)置為false留凭,不能被可以被刪除或不可以重新設(shè)置特性佃扼。默認(rèn)為false。這個(gè)屬性起到兩個(gè)作用:1蔼夜、目標(biāo)屬性是否可以使用delete刪除 2兼耀、目標(biāo)屬性是否可以再次設(shè)置特性
存取描述:當(dāng)使用存取器描述屬性的特性的時(shí)候,允許設(shè)置以下特性屬性
- get:屬性的 getter 函數(shù)求冷,如果沒(méi)有 getter瘤运,則為 undefined。當(dāng)訪問(wèn)該屬性時(shí)匠题,會(huì)調(diào)用此函數(shù)拯坟。執(zhí)行時(shí)不傳入任何參數(shù),但是會(huì)傳入 this 對(duì)象(由于繼承關(guān)系韭山,這里的this并不一定是定義該屬性的對(duì)象)郁季。該函數(shù)的返回值會(huì)被用作屬性的值。默認(rèn)為 undefined钱磅。
- set:屬性的 setter 函數(shù)梦裂,如果沒(méi)有 setter,則為 undefined盖淡。當(dāng)屬性值被修改時(shí)年柠,會(huì)調(diào)用此函數(shù)。該方法接受一個(gè)參數(shù)(也就是被賦予的新值)褪迟,會(huì)傳入賦值時(shí)的 this 對(duì)象冗恨。默認(rèn)為 undefined。
【3】示例
- value
let obj = {}
// 不設(shè)置value屬性
Object.defineProperty(obj, "name", {});
console.log(obj.name); // undefined
// 設(shè)置value屬性
Object.defineProperty(obj, "name", {
value: "Demi"
});
console.log(obj.name); // Demi
- writable
let obj = {}
// writable設(shè)置為false味赃,不能重寫
Object.defineProperty(obj, "name", {
value: "Demi",
writable: false
});
//更改name的值(更改失斉山)
obj.name = "張三";
console.log(obj.name); // Demi
// writable設(shè)置為true,可以重寫
Object.defineProperty(obj, "name", {
value: "Demi",
writable: true
});
//更改name的值
obj.name = "張三";
console.log(obj.name); // 張三
- enumerable
let obj = {}
// enumerable設(shè)置為false洁桌,不能被枚舉。
Object.defineProperty(obj, "name", {
value: "Demi",
writable: false,
enumerable: false
});
// 枚舉對(duì)象的屬性
for (let attr in obj) {
console.log(attr);
}
// enumerable設(shè)置為true侯嘀,可以被枚舉另凌。
Object.defineProperty(obj, "age", {
value: 18,
writable: false,
enumerable: true
});
// 枚舉對(duì)象的屬性
for (let attr in obj) {
console.log(attr); //age
}
- **configurable **
//-----------------測(cè)試目標(biāo)屬性是否能被刪除------------------------//
let obj = {}
// configurable設(shè)置為false谱轨,不能被刪除。
Object.defineProperty(obj, "name", {
value: "Demi",
writable: false,
enumerable: false,
configurable: false
});
// 刪除屬性
delete obj.name;
console.log(obj.name); // Demi
// configurable設(shè)置為true吠谢,可以被刪除土童。
Object.defineProperty(obj, "age", {
value: 19,
writable: false,
enumerable: false,
configurable: true
});
// 刪除屬性
delete obj.age;
console.log(obj.age); // undefined
//-----------------測(cè)試是否可以再次修改特性------------------------//
let obj2 = {}
// configurable設(shè)置為false,不能再次修改特性工坊。
Object.defineProperty(obj2, "name", {
value: "dingFY",
writable: false,
enumerable: false,
configurable: false
});
//重新修改特性
Object.defineProperty(obj2, "name", {
value: "張三",
writable: true,
enumerable: true,
configurable: true
});
console.log(obj2.name); // 報(bào)錯(cuò):Uncaught TypeError: Cannot redefine property: name
// configurable設(shè)置為true献汗,可以再次修改特性。
Object.defineProperty(obj2, "age", {
value: 18,
writable: false,
enumerable: false,
configurable: true
});
// 重新修改特性
Object.defineProperty(obj2, "age", {
value: 20,
writable: true,
enumerable: true,
configurable: true
});
console.log(obj2.age); // 20
- set 和 get
let obj = {
name: 'Demi'
};
Object.defineProperty(obj, "name", {
get: function () {
//當(dāng)獲取值的時(shí)候觸發(fā)的函數(shù)
console.log('get...')
},
set: function (newValue) {
//當(dāng)設(shè)置值的時(shí)候觸發(fā)的函數(shù),設(shè)置的新值通過(guò)參數(shù)value拿到
console.log('set...', newValue)
}
});
//獲取值
obj.name // get...
//設(shè)置值
obj.name = '張三'; // set... 張三
二王污、Proxy
Proxy 對(duì)象用于定義基本操作的自定義行為(如屬性查找罢吃,賦值,枚舉昭齐,函數(shù)調(diào)用等)
其實(shí)就是在對(duì)目標(biāo)對(duì)象的操作之前提供了攔截尿招,可以對(duì)外界的操作進(jìn)行過(guò)濾和改寫,修改某些操作的默認(rèn)行為阱驾,這樣我們可以不直接操作對(duì)象本身就谜,而是通過(guò)操作對(duì)象的代理對(duì)象來(lái)間接來(lái)操作對(duì)象,達(dá)到預(yù)期的目的~
【1】語(yǔ)法
const p = new Proxy(target, handler)
【2】參數(shù)
target:要使用 Proxy 包裝的目標(biāo)對(duì)象(可以是任何類型的對(duì)象里覆,包括原生數(shù)組丧荐,函數(shù),甚至另一個(gè)代理)
handler:也是一個(gè)對(duì)象喧枷,其屬性是當(dāng)執(zhí)行一個(gè)操作時(shí)定義代理的行為的函數(shù)虹统,也就是自定義的行為
【3】handler方法
handler 對(duì)象是一個(gè)容納一批特定屬性的占位符對(duì)象。它包含有 Proxy 的各個(gè)捕獲器(trap)割去,所有的捕捉器是可選的窟却。如果沒(méi)有定義某個(gè)捕捉器,那么就會(huì)保留源對(duì)象的默認(rèn)行為呻逆。
handler.getPrototypeOf() ===》 Object.getPrototypeOf 方法的捕捉器
handler.setPrototypeOf() ===》 Object.setPrototypeOf 方法的捕捉器
handler.isExtensible() ===》 Object.isExtensible 方法的捕捉器
handler.preventExtensions() ===》 Object.preventExtensions 方法的捕捉器
handler.getOwnPropertyDescriptor() ===》 Object.getOwnPropertyDescriptor 方法的捕捉器
handler.defineProperty() ===》 Object.defineProperty 方法的捕捉器
handler.has() ===》 in 操作符的捕捉器
handler.get() ===》 屬性讀取操作的捕捉器
handler.set() ===》 屬性設(shè)置操作的捕捉器
handler.deleteProperty() ===》 delete 操作符的捕捉器
handler.ownKeys() ===》 Object.getOwnPropertyNames方法和 Object.getOwnPropertySymbols 方法的捕捉器
handler.apply() ===》 函數(shù)調(diào)用操作的捕捉器
handler.construct() ===》 new 操作符的捕捉器
【4】示例
let obj = {
name: 'name',
age: 18
}
let p = new Proxy(obj, {
get: function (target, property, receiver) {
console.log('get...')
},
set: function (target, property, value, receiver) {
console.log('set...', value)
}
})
p.name // get...
p = {
name: 'dingFY',
age: 20
}
// p.name = '張三' // set... 張三
三夸赫、defineProperty和Proxy對(duì)比
- Object.defineProperty只能劫持對(duì)象的屬性,而Proxy是直接代理對(duì)象咖城。
由于 Object.defineProperty 只能對(duì)屬性進(jìn)行劫持茬腿,需要遍歷對(duì)象的每個(gè)屬性,如果屬性值也是對(duì)象宜雀,則需要深度遍歷切平。而 Proxy 直接代理對(duì)象,不需要遍歷操作辐董。
- Object.defineProperty對(duì)新增屬性需要手動(dòng)進(jìn)行Observe悴品。
由于 Object.defineProperty 劫持的是對(duì)象的屬性,所以新增屬性時(shí),需要重新遍歷對(duì)象(改變屬性不會(huì)自動(dòng)觸發(fā)setter)苔严,對(duì)其新增屬性再使用 Object.defineProperty 進(jìn)行劫持定枷。
也正是因?yàn)檫@個(gè)原因,使用vue給 data 中的數(shù)組或?qū)ο笮略鰧傩詴r(shí)届氢,需要使用 vm.$set 才能保證新增的屬性也是響應(yīng)式的欠窒。
- defineProperty會(huì)污染原對(duì)象(關(guān)鍵區(qū)別)
proxy去代理了ob,他會(huì)返回一個(gè)新的代理對(duì)象不會(huì)對(duì)原對(duì)象ob進(jìn)行改動(dòng)退子,而defineproperty是去修改元對(duì)象岖妄,修改元對(duì)象的屬性,而proxy只是對(duì)元對(duì)象進(jìn)行代理并給出一個(gè)新的代理對(duì)象寂祥。
四荐虐、簡(jiǎn)單實(shí)現(xiàn)數(shù)據(jù)雙向綁定
【1】新建myVue.js文件,創(chuàng)建myVue類
class myVue extends EventTarget {
constructor(options) {
super();
this.$options = options;
this.compile();
this.observe(this.$options.data);
}
// 數(shù)據(jù)劫持
observe(data) {
let keys = Object.keys(data);
// 遍歷循環(huán)data數(shù)據(jù)壤靶,給每個(gè)屬性增加數(shù)據(jù)劫持
keys.forEach(key => {
this.defineReact(data, key, data[key]);
})
}
// 利用defineProperty 進(jìn)行數(shù)據(jù)劫持
defineReact(data, key, value) {
let _this = this;
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
return value;
},
set(newValue) {
// 監(jiān)聽(tīng)到數(shù)據(jù)變化缚俏, 觸發(fā)事件
let event = new CustomEvent(key, {
detail: newValue
});
_this.dispatchEvent(event);
value = newValue;
}
});
}
// 獲取元素節(jié)點(diǎn),渲染視圖
compile() {
let el = document.querySelector(this.$options.el);
this.compileNode(el);
}
// 渲染視圖
compileNode(el) {
let childNodes = el.childNodes;
// 遍歷循環(huán)所有元素節(jié)點(diǎn)
childNodes.forEach(node => {
if (node.nodeType === 1) {
// 如果是標(biāo)簽 需要跟進(jìn)元素attribute 屬性區(qū)分v-html 和 v-model
let attrs = node.attributes;
[...attrs].forEach(attr => {
let attrName = attr.name;
let attrValue = attr.value;
if (attrName.indexOf("v-") === 0) {
attrName = attrName.substr(2);
// 如果是 html 直接替換為將節(jié)點(diǎn)的innerHTML替換成data數(shù)據(jù)
if (attrName === "html") {
node.innerHTML = this.$options.data[attrValue];
} else if (attrName === "model") {
// 如果是 model 需要將input的value值替換成data數(shù)據(jù)
node.value = this.$options.data[attrValue];
// 監(jiān)聽(tīng)input數(shù)據(jù)變化贮乳,改變data值
node.addEventListener("input", e => {
this.$options.data[attrValue] = e.target.value;
})
}
}
})
if (node.childNodes.length > 0) {
this.compileNode(node);
}
} else if (node.nodeType === 3) {
// 如果是文本節(jié)點(diǎn)忧换, 直接利用正則匹配到文本節(jié)點(diǎn)的內(nèi)容,替換成data的內(nèi)容
let reg = /\{\{\s*(\S+)\s*\}\}/g;
let textContent = node.textContent;
if (reg.test(textContent)) {
let $1 = RegExp.$1;
node.textContent = node.textContent.replace(reg, this.$options.data[$1]);
// 監(jiān)聽(tīng)數(shù)據(jù)變化向拆,重新渲染視圖
this.addEventListener($1, e => {
let oldValue = this.$options.data[$1];
let reg = new RegExp(oldValue);
node.textContent = node.textContent.replace(reg, e.detail);
})
}
}
})
}
}
【2】在html文件中引入myVue.js亚茬, 創(chuàng)建實(shí)例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script src="./mvvm.js" type="text/javascript"></script>
<title>Document</title>
</head>
<body>
<div id="app">
<div>我的名字叫:{{name}}</div>
<div v-html="htmlData"></div>
<input v-model="modelData" /> {{modelData}}
</div>
</body>
<script>
let vm = new myVue({
el: "#app",
data: {
name: "Demi",
htmlData: "html數(shù)據(jù)",
modelData: "input的數(shù)據(jù)"
}
})
</script>
</html>
【3】效果
文章每周持續(xù)更新,可以微信搜索「 前端大集錦 」第一時(shí)間閱讀浓恳,回復(fù)【視頻】【書(shū)籍】領(lǐng)取200G視頻資料和30本PDF書(shū)籍資料