一:數據代理
1.什麼是數據代理
- 數據代理是指通過一個對象代理對另一個對象(在前一個對象內部)中屬性的操作(讀/寫)。
2.實現示例
-
被代理對象:自有 屬性的查詢【Object.keys(obj)】
-
代理對象:存取器 屬性的設置【Object.defineProperties(obj,attr,options)】
let obj = {username:"zhangsan",password:"123456"};
let proxy = {};
Object.keys(obj).forEach((key)=>{
Object.defineProperty(proxy,key,{
configurable:false,// 不可再次配置該屬性
enumerable:true,// 可枚舉該屬性
get: function proxyGetter() {
return obj[key]; // 定義爲存取器屬性,通過getter方法代理對obj的讀操作。
},
set: function proxySetter(newVal) {
obj[key] = newVal;// 定義爲存取器屬性,通過setter方法代理對obj的寫操作。
}
})
});
3.運行結果【代理成功】
二:模板解析
1.要解析的view
<div id="template">
<H1>Template Compile Test</H1>
<H1>{{username}}</H1>
<H1>{{password}}</H1>
</div>
2.要解析的model
let data = {
username:"zhangsan",
password : "123456"
};
3.解析過程
- 創建documentFragment容器(內存中)
// 1.創建documentFragment(內存中的node容器)
let fragment = document.createDocumentFragment();
- 導出document中template區域內的節點並填充至documentFragment容器
// 2.導出document中template區域內的節點並填充至documentFragment
let template = document.getElementById('template');
let node;
while(node = template.firstChild){
// dom節點只能有一個父節點,添加到fragment時,dom就會移除該節點,循環控制template.firstChild的指向會不斷後移
fragment.appendChild(node)
}
- 編譯解析documentFragment容器中的節點
// 3.編譯解析documentFragment
let nodeArr = [].slice.call(fragment.childNodes);// 僞數組轉數組準備遍歷
nodeArr.forEach((node)=>{
// 注意:爲簡化,只解析第一層元素節點的文本節點。
if(node.nodeType==1){// 3.1 找到要解析的位置
let oldText = node.firstChild.textContent;// 3.2 獲取解析前的值oldText
var reg = /\{\{(.*)\}\}/;
if(reg.test(oldText)){
let exp = RegExp.$1;
let newText = data[exp];// 3.3 獲取解析後的值
node.textContent = typeof newText=='undefined'?'':newText;// 3.4使用newText覆蓋oldText
}
}
});
- 將解析後的documentFragment填充至document的template區域內
// 4.將解析後的documentFragment填充至document的template中
template.appendChild(fragment);
4.運行結果
三:數據綁定
數據綁定綁定的雙方是view和model,model->view的初始化綁定也即上面的模板解析操作,下面探討model->view的更新綁定。
1.實現方案:數據劫持技術
- 指的是在訪問或者修改對象的某個屬性時,通過一段代碼攔截這個行爲,進行額外的操作或者修改返回結果。
- 從上也即得到思路,所有需要被劫持的屬性都不能爲數據屬性,而應該修改爲存取器屬性(可通過Object.definePropty方法實現)。
2.實現示例
- 複製一份模板解析代碼
- 爲username和password添加id以便獲取
<div id="template">
<H1>Template Compile Test</H1>
<H1 id="username">{{username}}</H1>
<H1 id="password">{{password}}</H1>
</div>
- 添加如下數據劫持代碼(將需要更新的model數據都從數據屬性變爲存取器屬性)
// ...,這裏以上爲模板解析的代碼
// 這裏爲簡化:使用mvMap實現view與model一對一的映射,而view與model實際上應是多對多的關係,
let mvMap = {
username:document.getElementById('username'),
password:document.getElementById('password')
};
Object.keys(data).forEach((key)=>{
dataAttr2GetSetAttr(data,key,data[key])
});
function dataAttr2GetSetAttr(data,key,val){
Object.defineProperty(data,key,{
configurable:false,
enumerable:true,
get:function bindGetter(){
return val
},
set:function bindSetter(newVal){
val= newVal;// 注意:這裏不能使用data[key],不然會一直自調用死循環。
// model發生更新,通知view更新。
mvMap[key].textContent = typeof newVal=='undefined'?'':newVal;
}
})
}
3.運行結果(第一步)
4.運行結果(第二步)
四:雙向數據綁定
由以上模板解析和數據綁定的實踐實例瞭解了model->view的單向綁定,接下只需加上view->model的單向綁定即可實現雙向綁定。
1.實現方案:借用事件機制訪問model
2.實現示例
- 複製三:數據綁定產生的代碼
- view上加上input輸入框,如下
<input type="text" id="input">
<div id="template">
<H1>Template Compile Test</H1>
<H1 id="username">{{username}}</H1>
<H1 id="password">{{password}}</H1>
</div>
- 爲該輸入框添加事件
let inputNode = document.getElementById('input');
inputNode.addEventListener('input', function (e) {
let oldVal = data.username;
var newValue = e.target.value;
if (oldVal === newValue) {
return;
}
data.username = newValue;
});
3.運行結果
二三四最終生成的html測試文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>雙向數據綁定</title>
</head>
<body>
<input type="text" id="input">
<div id="template">
<H1>Template Compile Test</H1>
<H1 id="username">{{username}}</H1>
<H1 id="password">{{password}}</H1>
</div>
<script type="text/javascript">
let data = {
username:"zhangsan",
password : "123456"
};
// 二:模板解析
// 1.創建documentFragment(內存中的node容器)
let fragment = document.createDocumentFragment();
// 2.導出document中template區域內的節點並填充至documentFragment
let template = document.getElementById('template');
let node;
while(node = template.firstChild){
// dom節點只能有一個父節點,添加到fragment時,dom就會移除該節點,循環控制template.firstChild的指向會不斷後移
fragment.appendChild(node)
}
// 3.編譯解析documentFragment
let nodeArr = [].slice.call(fragment.childNodes);// 僞數組轉數組準備遍歷
nodeArr.forEach((node)=>{
// 注意:爲簡化,只解析第一層元素節點的文本節點。
if(node.nodeType==1){// 3.1 找到要解析的位置
let oldText = node.firstChild.textContent;// 3.2 獲取解析前的值oldText
var reg = /\{\{(.*)\}\}/;
if(reg.test(oldText)){
let exp = RegExp.$1;
let newText = data[exp];// 3.3 獲取解析後的值
node.textContent = typeof newText=='undefined'?'':newText;// 3.4使用newText覆蓋oldText
}
}
});
// 4.將解析後的documentFragment填充至document的template中
template.appendChild(fragment);
// 三:數據綁定
// 這裏爲簡化:使用mvMap實現view與model一對一的映射,而view與model實際上應是多對多的關係,
let mvMap = {
username:document.getElementById('username'),
password:document.getElementById('password')
};
Object.keys(data).forEach((key)=>{
dataAttr2GetSetAttr(data,key,data[key])
});
function dataAttr2GetSetAttr(data,key,val){
Object.defineProperty(data,key,{
configurable:false,
enumerable:true,
get:function bindGetter(){
return val
},
set:function bindSetter(newVal){
val= newVal;// 注意:這裏不能使用data[key],不然會一直自調用死循環。
// model發生更新,通知view更新。
mvMap[key].textContent = typeof newVal=='undefined'?'':newVal;
}
})
}
// 四:雙向數據綁定
let inputNode = document.getElementById('input');
inputNode.addEventListener('input', function (e) {
let oldVal = data.username;
var newValue = e.target.value;
if (oldVal === newValue) {
return;
}
data.username = newValue;
});
</script>
</body>
</html>