概念
双向绑定概念其实很简略,就是视图(View)的变动能实时让数据模型(Model)发生变化,而数据的变动也能实时更新到视图层。咱们所说的单向数据绑定就是从数据到视图这一方向的关系。
剖析
1、响应式数据
应用 Object.defineProperty、Proxy 对数据进行监听拦挡。
//obj:必须。指标对象
//prop:必须。需定义或批改的属性的名字
//descriptor:必须。指标属性所领有的个性
Object.defineProperty(obj, prop, descriptor)
vue3.0 开始 Proxy 代替 Object.defineProperty
let p = new Proxy(target, handler);
2、input 事件监听
绑定事件处理函数,实时批改数据。
3、相干 dom 操作
将数据与相干 dom 节点绑定在一起,批改数据的时候对应的 dom 节点也应一起扭转。
实现
1、html 页面
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>v-model</title>
</head>
<body>
<div id="app">
<input type="text" v-model="name" placeholder="姓名"/>
<input type="text" v-model="age" placeholder="年龄"/>
<div>
<p>
姓名:<span>{{name}}</span>
</p>
<p>
年龄:<span>{{age}}</span>
</p>
<p>
<p>
年龄:<span>{{age}}</span>
</p>
</p>
</div>
<button id="btn"> 批改名字 </button>
</div>
</body>
<script src="./VModel.js"></script>
<script>
const app = new VModel('#app',{
name:'',
age:''
});
document.getElementById('btn').addEventListener('click',function() {app.setData('name','名字扭转了');
})
</script>
</html>
2、VModel.js
(1)结构 VModel
class VModel {constructor(el,data) {this.el = document.querySelector(el);
// 存放数据对象
this._data = data;
// 寄存绑定数据的 dom 节点
this.domPoll = {};
this.init();}
}
(2)初始化数据对象
应用 Object.defineProperty
initData () {
const _this = this;
this.data = {};
for(let key in this._data){
Object.defineProperty(this.data,key,{get(){console.log("获取数据",key,_this._data[key]);
return _this._data[key];
},
set(newVal){console.log("设置数据",key,newVal);
_this.domPoll[key].innerText = newVal;
_this._data[key] = newVal;
}
});
}
}
应用 Proxy
initData () {
const _this = this;
this.data = {};
this.data = new Proxy(this.data,{get(target,key){return Reflect.get(target,key);
},
set(target,key,value){// _this.domPoll[key].innerText = value;
_this.domPoll[key].forEach(item => {item.innerText = value;})
return Reflect.set(target,key,value);
}
})
}
(3)绑定 dom 节点
bindDom(el){
const childNodes = el.childNodes;
childNodes.forEach(item => {
//nodeType 为 3 时该 dom 节点为文本节点
if(item.nodeType === 3){
const _value = item.nodeValue;
if(_value.trim().length){
// 匹配是否有两个花括号包裹的数据
let _isValid = /\{\{(.+?)\}\}/.test(_value);
if(_isValid){const _key = _value.match(/\{\{(.+?)\}\}/)[1].trim();
// this.domPoll[_key] = item.parentNode;
// 一个数据能够被多个 dom 节点绑定,所以应该用数组来进行保留
// 未定义时先初始化
if(!this.domPoll[_key]) this.domPoll[_key] = [];
this.domPoll[_key].push(item.parentNode);
// 替换绑定的值
item.parentNode.innerText = this.data[_key] || undefined;
}
}
}
// 递归遍历子节点
item.childNodes && this.bindDom(item);
})
}
(4)输入框数据绑定
bindInput(el){
// 获取 input 所有元素节点
const _allInput = el.querySelectorAll('input');
_allInput.forEach(input => {const _vModel = input.getAttribute('v-model');
// 判断是否有 v -model 属性
if(_vModel){// 监听输出事件 input.addEventListener('keyup',this.handleInput.bind(this,_vModel,input),false);
}
})
}
handleInput(key,input){
const _value = input.value;
// 数据变动的时候会同步批改 dom 节点绑定的数据。this.data[key] = _value;
}
(4)残缺代码
class VModel {constructor(el,data) {this.el = document.querySelector(el);
this._data = data;
this.domPoll = {};
this.init();}
init(){this.initData();
this.initDom();}
initDom(){this.bindDom(this.el);
this.bindInput(this.el);
console.log('domPoll',this.domPoll);
}
initData () {
const _this = this;
this.data = {};
// for(let key in this._data){
// Object.defineProperty(this.data,key,{// get(){// console.log("获取数据",key,_this._data[key]);
// return _this._data[key];
// },
// set(newVal){// console.log("设置数据",key,newVal);
// _this.domPoll[key].innerText = newVal;
// _this._data[key] = newVal;
// }
// });
// }
this.data = new Proxy(this.data,{get(target,key){return Reflect.get(target,key);
},
set(target,key,value){// _this.domPoll[key].innerText = value;
_this.domPoll[key].forEach(item => {item.innerText = value;})
return Reflect.set(target,key,value);
}
})
}
bindDom(el){
const childNodes = el.childNodes;
childNodes.forEach(item => {if(item.nodeType === 3){
const _value = item.nodeValue;
if(_value.trim().length){let _isValid = /\{\{(.+?)\}\}/.test(_value);
if(_isValid){const _key = _value.match(/\{\{(.+?)\}\}/)[1].trim();
// this.domPoll[_key] = item.parentNode;
if(!this.domPoll[_key]) this.domPoll[_key] = [];
this.domPoll[_key].push(item.parentNode);
item.parentNode.innerText = this.data[_key] || undefined;
}
}
}
item.childNodes && this.bindDom(item);
})
}
bindInput(el){const _allInput = el.querySelectorAll('input');
_allInput.forEach(input => {const _vModel = input.getAttribute('v-model');
if(_vModel){input.addEventListener('keyup',this.handleInput.bind(this,_vModel,input),false);
}
})
}
handleInput(key,input){
const _value = input.value;
this.data[key] = _value;
// console.log(this.data);
}
setData(key,value){this.data[key] = value;
}
}