一:数据代理
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>