目錄
2.ES5的Object.defineProperty()實現數據劫持
2.1Object.defineProperty()下的配置
2.2使用Object.defineProperty()方式模擬Vue數據劫持,並更新視圖(數據雙向綁定)
1.課堂主題及知識點
##課堂主題
- 利用defineProperty實現數據劫持;
- 利用ES6中proxy實現數據劫持
- 數據劫持實現mvvm裏的表達式
- 利用自定義事件實現數據動態更新;
- 通過es6模塊化改造自己的mvvm框架;
- AMD模塊化require.js介紹;
##知識點
- defineProperty;
- Proxy代理
- 數據劫持
- es6模塊化、exports 和 import
- AMD /CMD模塊化;
- MVVM框架:數據驅動,數據優先(數據變化,視圖也跟着變化)
數據在前端使用得最多的即對象數組,如[{},{},{}]形式。
數據劫持:攔截數據變化,再將變化的數據更新到視圖。
2.ES5的Object.defineProperty()實現數據劫持
- 參數一:被劫持的對象數據;
- 參數二:要劫持的對象數據中的屬性;
- 參數三:對象,裏面的get()/set()方法在數據改變時會自動監聽並執行(即數據劫持)
- 每個屬性劫持都需要單獨使用Object.defineProperty()進行設置劫持,劫持後的數據都有自己的get()/set()方法(如obj.name會自動調用get()方法,obj.name = 'zs'時會自動調用set()方法),其返回值仍然會被劫持。
Object.defineProperty(obj,'name',{
configurable:true,
enumerable:true,
get(){
return value;
},
set(newValue){
console.log("set...");
value = newValue;
}
})
2.1Object.defineProperty()下的配置
- configurable:true,表示可配置,即是否可更改,默認爲true。如果設置了false,則delete Object.key(name)則不會生效;
- enumeable:true,表示可枚舉,默認爲true。如果設置爲false,則會影響for in循環,Object.keys(),JSON.stringfy(),Object.assign()等方法的使用。
2.2使用Object.defineProperty()方式模擬Vue數據劫持,並更新視圖(數據雙向綁定)
Vue案例:更改數據vue.message= "hello my vue"時,視圖也會隨之更改
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">{{message}}</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
let vue = new Vue({
el: "#app",
data:{
message:"hello vue"
}
})
</script>
</body>
</html>
效果:
Object.defineProperty()方式模擬Vue數據劫持使用及步驟解析:
- 通過document.querySelect(el)找到被數據劫持範圍內所有節點(nodes = eles.childNodes)。並進行初次視圖渲染;
- 通過node.nodeType===3或node.nodeType===1判斷節點爲文本還是節點,如果是文本節點直接使用正則let reg = /\{\{\s*(\S+)\s*\}\}/g 將正則中的組(\S+)替換爲要替換的數據(組內內容使用$1獲取)。node.textContent().replace(reg,value);
- 判斷node.childNodes.length大於0則表示還有 子節點,需要使用遞歸實現多層節點替換message;不大於0則表示不再有子節點;
- 使用Object.defineProperty()方法實現數據劫持並改變數據;
- 使用自定義事件(繼承TargetEvent,並使用CustomEvent類)實現視圖的再次渲染,通過let event = new CustomEvent(key,value);this.dispatchEvent(event)監聽數據變化(注意此處this和自定義事件this的使用),並將設置的值獲取並替換上一次的值即let oldValue = this.options.dada[$1]; let reg = new RegExt(oldValue,"g"); node.textContent = node.textContent.replace(reg,newValue);
- 問題:不支持多層數據劫持更新,且多次更新也有問題
- 輸入框中使用屬性v-html = "htmldata"指令進行更新。獲取節點中所有屬性node.attributes ,然後循環所有屬性attr.name attr.value ,使用let attrName = attr.name.substr(2)去掉v-html的‘v-’;然後判斷attrName = "html"時 node.innerHTML = this.options.data[attrValue];attrName = "model" 時node.value = this.options.data[attrValue];
- 因爲使用的數據雙綁定,所以在input輸入框中輸入時也需要將值綁定並更新到視圖中。在CustomEvent中通過e.detail獲取input的值,然後直接賦值,因爲已經對數據進行劫持,並且進行了二次渲染,所以賦值後會直接進行數據劫持和視圖更新
- 通過new Proxy()方法同樣實現數據劫持和視圖更新
案例實現Vue模擬:
- 因爲傳入的el是#app所以用document.querySelect(el)而不是document.querySelectAll(el);
- 自定義事件的使用:增加監聽事件;使用CustomEvent獲取監聽事件及傳遞的數據;dipatchEvent(event)觸發事件;觸發自定義事件時的this指向問題;
- v-html和v-model指令更新時,是獲取v-html和v-model所在節點的屬性值,且此處做input輸入框數據雙向綁定時,監聽事件是加在node節點上,而不是this上(因爲繼承了TargetEvent,所以此處this指向TargetEvent,所以不能使用)
Kvue.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="Kvue-new-Proxy.js"></script>
<title>Document</title>
</head>
<body>
<div id="app">
{{message}}外層
<div>
<span>{{message}}</span>
</div>
{{code}}
<!-- 注意所有v-html或 v-model都必須在el屬性範圍內-->
<div v-html="htmlData"></div>
<!-- 數據雙綁定——輸入框輸入內容的數據也會被劫持 -->
<input v-model="modelData"/> {{modelData}}
</div>
<script>
let kVue = new Kvue({
el: '#app',
data: {
message: "這是我的Kvue!",
code: 303,
// 注意此處屬性值必須和佈局中v-html 和v-model後面的屬性值保持一致
htmlData:"html數據",
modelData:"數據雙綁定"
}
});
//監聽獲取數據
// console.log(kVue.data);
</script>
</body>
</html>
Kvue.js:
/**
* 模擬實現vue功能
* 功能三:輸入框中使用屬性v-html = "htmldata"指令進行更新
*/
class Kvue extends EventTarget {
//options表示
constructor(options) {
super();
this.options = options;
this.data = this.options.data;
this.compile();
// 數據劫持
this.observe(this.options.data);
}
// 監聽數據變化
observe(data) {
Object.keys(data).forEach(key => {//key即data中的屬性名
this.observeData(data, key, data[key]);
});
}
// 通過Object.defineProperty劫持數據變化
observeData(data, key, value) {
let _this = this;
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
console.log("get----");
return value;
},
set(newValue) {
console.log("set----", newValue);
let event = new CustomEvent(key, { detail: newValue });//參數一時間名,參數二傳遞的數據
// 需要觸發事件
_this.dispatchEvent(event);
return value = newValue;//set()方法返回更新後的數據
}
});
}
//處理數據,渲染視圖
compile() {
// 注意此處傳入的爲#app只有一個元素
let eles = document.querySelector(this.options.el);
this.compileNode(eles.childNodes);
}
compileNode(childNodes) {
childNodes.forEach(node => {
// 循環所有nodes,當nodeType爲1時表示爲節點,需找到下一個節點直到找到的是文本;爲3表示爲文本直接正則匹配顯示
if (node.nodeType === 1) {
// 數據雙綁定
let attrs = node.attributes;//獲取屬性及屬性值
console.log(attrs);
[...attrs].forEach(attr => {
let attrName = attr.name;
let attrValue = attr.value;
// 獲取v-html v-model的v-後面的內容
attrName = attrName.substr(2);
console.log(attrName,attrValue);
// 爲html時,通過innerHTML獲取設置值
if (attrName == "html") {
node.innerHTML = this.data[attrValue];
// 爲model時,是input框 通過value獲取設置值
} else if (attrName == "model") {
// 注意是給當前節點設置值和加監聽,所以使用node,次此處this指向EventTarget
// 給input數據框設置初始值
node.value = this.data[attrValue];
// 監聽input更改後更新input視圖
node.addEventListener("input",e=>{
console.log("attrValue設置了值;", e.target.value);//此處是input框,通過e.target.value獲取值
this.data.modelData = e.target.value;
});
}
});
// 判斷nodes.length>0則表示還有子節點,需要遞歸找到下一個節點直到找到的是文本;nodes.length不大於0表示只有一個文本節點,直接匹配顯示
if (node.childNodes.length > 0) {
this.compileNode(node.childNodes);
}
} else if (node.nodeType === 3) {
let reg = /\{\{\s*(\S+)\s*\}\}/g;//\{ \}表示精確匹配{},\s*表示message前後可能會有任意個空格,(\S+)表示用組匹配message
let textContent = node.textContent;
let test = reg.test(textContent);//注意要使用test方法測試是否匹配再替換
if (test) {
// 初次渲染;
let $1 = RegExp.$1;
node.textContent = textContent.replace(reg, this.options.data[$1]);
// 頁面再次渲染
//設置監聽事件,監聽$1即每個data中的key
// 注意:自定義事件的用法,因爲此處設置了監聽,所以在observeData方法中,dispatchEvent(event)觸發事件時會自動被監聽
// 所以此處的e即CustomEvent對象,所以有e.detail屬性
// console.log(this);//EventTarget {options: {…}, data: {…}}
this.addEventListener($1, e => {
console.log("設置了值;", e.detail);
// 獲取設置的值
let newValue = e.detail;
// 原來數據中的值key ,如message
let oldValue = this.options.data[$1];
// 全局匹配所有的原有數據
let reg = new RegExp(oldValue, "g");
// 將原有數據改爲更改後的值
node.textContent = node.textContent.replace(reg, newValue);
});
}
}
});
}
}
3.ES6中new Proxy()實現數據劫持
-
定義 :對象用於定義基本操作的自定義行爲(如屬性查找,賦值,枚舉,函數調用等)。
-
基本使用
let obj = new Proxy({
name: "張三",
age: 20
},{
//target即傳入的原始數據即第一個參數值
get(target, name) {
return target[name];
},
set(target,name,value){
target[name] = value;
}
})
-
相關配置參數
has(target, propKey):攔截propKey in proxy的操作,返回一個布爾值。
defineProperty(target, propKey, propDesc):攔截Object.defineProperty(proxy, propKey, propDesc)
Object.defineProperties(proxy, propDescs),返回一個布爾值。
使用new Proxy()實現模擬Vue:
Kvue.html同上;
Kvue-new-proxy.js:
- new Proxy()需要使用this.data進行接收;
- set()和get()中的target即new Proxy()時傳入的第一個參數值(即原始數據);
- 仍然需要觸發監聽
/**
* 模擬實現vue功能
* 功能四:通過new Proxy實現和Object.property()同樣功能
*/
class Kvue extends EventTarget {
//options表示
constructor(options) {
super();
this.options = options;
this.data = this.options.data;
this.compile();
// 數據劫持
this.observe(this.options.data);
}
// 監聽數據變化
observe(data) {
// Object.keys(data).forEach(key => {//key即data中的屬性名
// this.observeData(data, key, data[key]);
// });
let _this = this;
this.data = new Proxy(data, {
// target即傳入的data原始數據
get(target, key) {
return target[key];
},
// 此處newValue即傳入的改變的數據
set(target, key, newValue) {
console.log(target);
let event = new CustomEvent(key, { detail: newValue });//參數一時間名,參數二傳遞的數據
// 需要觸發事件
_this.dispatchEvent(event);
return target[key] = newValue;
}
});
}
// 通過Object.defineProperty劫持數據變化
// observeData(data, key, value) {
// let _this = this;
// Object.defineProperty(data, key, {
// configurable: true,
// enumerable: true,
// get() {
// console.log("get----");
// return value;
// },
// set(newValue) {
// console.log("set----", newValue);
// let event = new CustomEvent(key, { detail: newValue });//參數一時間名,參數二傳遞的數據
// // 需要觸發事件
// _this.dispatchEvent(event);
// return value = newValue;//set()方法返回更新後的數據
// }
// });
// }
//處理數據,渲染視圖
compile() {
// 注意此處傳入的爲#app只有一個元素
let eles = document.querySelector(this.options.el);
this.compileNode(eles.childNodes);
}
compileNode(childNodes) {
childNodes.forEach(node => {
// 循環所有nodes,當nodeType爲1時表示爲節點,需找到下一個節點直到找到的是文本;爲3表示爲文本直接正則匹配顯示
if (node.nodeType === 1) {
// 數據雙綁定
let attrs = node.attributes;//獲取屬性及屬性值
console.log(attrs);
[...attrs].forEach(attr => {
let attrName = attr.name;
let attrValue = attr.value;
// 獲取v-html v-model的v-後面的內容
attrName = attrName.substr(2);
console.log(attrName, attrValue);
// 爲html時,通過innerHTML獲取設置值
if (attrName == "html") {
node.innerHTML = this.data[attrValue];
// 爲model時,是input框 通過value獲取設置值
} else if (attrName == "model") {
// 注意是給當前節點設置值和加監聽,所以使用node,次此處this指向EventTarget
// 給input數據框設置初始值
node.value = this.data[attrValue];
// 監聽input更改後更新input視圖
node.addEventListener("input", e => {
console.log("attrValue設置了值;", e.target.value);//此處是input框,通過e.target.value獲取值
this.data.modelData = e.target.value;
});
}
});
// 判斷nodes.length>0則表示還有子節點,需要遞歸找到下一個節點直到找到的是文本;nodes.length不大於0表示只有一個文本節點,直接匹配顯示
if (node.childNodes.length > 0) {
this.compileNode(node.childNodes);
}
} else if (node.nodeType === 3) {
let reg = /\{\{\s*(\S+)\s*\}\}/g;//\{ \}表示精確匹配{},\s*表示message前後可能會有任意個空格,(\S+)表示用組匹配message
let textContent = node.textContent;
let test = reg.test(textContent);//注意要使用test方法測試是否匹配再替換
if (test) {
// 初次渲染;
let $1 = RegExp.$1;
node.textContent = textContent.replace(reg, this.options.data[$1]);
// 頁面再次渲染
//設置監聽事件,監聽$1即每個data中的key
// 注意:自定義事件的用法,因爲此處設置了監聽,所以在observeData方法中,dispatchEvent(event)觸發事件時會自動被監聽
// 所以此處的e即CustomEvent對象,所以有e.detail屬性
// console.log(this);//EventTarget {options: {…}, data: {…}}
this.addEventListener($1, e => {
console.log("設置了值;", e.detail);
// 獲取設置的值
let newValue = e.detail;
// 原來數據中的值key ,如message
let oldValue = this.options.data[$1];
// 全局匹配所有的原有數據
let reg = new RegExp(oldValue, "g");
// 將原有數據改爲更改後的值
node.textContent = node.textContent.replace(reg, newValue);
});
}
}
});
}
}
4.es6模塊化
- 瀏覽器默認模塊化 script 里加入 "type=module";
- 導出 關鍵字 export
- export 可以導出多個,export default 只能導出一個;
- 使用模塊化時,需要在服務器環境打開頁面,否則會報錯
4.1導出方式
4.1.1導出 方式一 :
export.js文件中
export { a ,b , c}
對應導入方式:
import {a,b,c} from './export.js';
4.1.2導出方式二 關鍵字 "as"
export { a as aa ,b , c}
對應導入方式:
import { aa, b, c } from './export.js';
console.log(aa, b, c);//10 20 30
4.1.3導出方式三
export let d = ()=>{console.log("I am d function...")}
對應導入方式:
import {d} from './export.js';
console.log(d);
d();//I am d function...
4.1.4導出方式四
// export default a;//等同export {a as default};
export {b as default};
對應導入方式:
// import a from './export.js';
// console.log(a);//10
import b from './export.js';
console.log(b);//20
4.2導入方式
導入方式:關鍵字 import,js文件名前必須加'./'
4.2.1export 使用對象導出的,命名要保持一致方式
import {aa , b , c} from './moduleb.js';
4.2.2export導出的,命名可以自定義方式;
import myfn from './moduleb.js';
4.2.3通配符 "*"方式導入
import * as obj from './moduleb.js';
ES6導入導出示例:
module.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module">
// 注意使用import模塊化時需要運行在服務器上,否則會報錯
// 導出方式一對應導入方式一:
// import {a,b,c} from './export.js';
// console.log(a, b, c);//10 20 30
// 導出方式二對應導入方式二:
// import { aa, b, c } from './export.js';
// console.log(aa, b, c);//10 20 30
// 導出方式三對應導入方式三:
// import {d} from './export.js';
// console.log(d);
// d();//I am d function...
// 導出方式四對應導入方式四:
// import a from './export.js';
// console.log(a);//10
// import b from './export.js';
// console.log(b);//20
// 導出方式五:可使用通配符*方式導入
import * as obj from './export.js';
console.log(obj);//得到的是整個模塊,Module {Symbol(Symbol.toStringTag): "Module"}可通過obj.a,obj.b獲取具體值
</script>
</body>
</html>
export.js:
let a = 10;
let b = 20;
let c = 30;
// 導出方式一:
export {a,b,c};
// 導出方式二:
// export {a as aa,b,c};
// 導出方式三:
// export let d = ()=>{console.log("I am d function...")}
// 導出方式四:
// export default a;//等同export {a as default};
// export {b as default};
5.AMD require.js的使用
5.1引入require.js
https://cdn.bootcss.com/require.js/2.3.6/require.js
5.2加載模塊
require(["a"]);
5.3定義模塊
5.3.1無依賴定義
define({
method1:function(){
console.log("a method...");
},
method2:function(){
console.log("b method...");
}
});
5.3.2模塊有依賴
define(["c"],{
method1:function(){
console.log("a method...");
},
method2:function(){
console.log("b method...");
}
});
5.3.3函數式寫法
define(["c"],function(){
obj = {
name:"張安",
age:20
}
return obj;
});
6.模塊化優點
- 防止作用域污染
- 提高代碼的複用性
- 維護成本降低
7.總結
- defineProperty
- Proxy
- 數據劫持
- 自定義事件
- es6模塊化
- AMD/CMD模塊化