实现原理
vue实现数据双向绑定主要是采用数据劫持结合发布者、订阅者的模式的方式来实现。通过Object.defineProperty()的get和set来劫持每个属性,在数据发生变化时通过发布者发消息给订阅者,触发相应的监听回掉。具体就是先把说有的数据做一个数据劫持。第一先修改数据,在input框输入值的时候会触发一个对应的方法。把对应的数据修改掉。第二修改视图,在视图中使用到的每一个数据绑定一个Watcher监听,在Object.defineProperty()中的get中把每个Watcher监听存储到订阅者的数组中,当Object.defineProperty()中的set响应到数据发生改变的时候通过发布者通知对应的Watcher,Watcher会在数据中获取对应的值通过Watcher回掉返回,通过node.textContent = 新值来改变视图。
html代码
<!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">
<title>Document</title>
</head>
<body>
<div id="box">
<input type="text" v-model="val">
<div>{{val}}
<p>{{list.sex}}</p>
</div>
<p>{{list.sex}}</p>
</div>
<script src="./mvvm.js"></script>
<script>
new Mvvm({
el:'box',
data:{
val:'梦亦',
list:{
sex:'男',
}
}
})
</script>
</body>
</html>
js代码
class Dep{
constructor(){
this.watcherList = [];
}
subscription(obj){ //订阅者
this.watcherList.push(obj);
}
issue(){ //发布者
this.watcherList.forEach(item=>{
item.sendVal();
})
}
}
Dep.target = null;
const dep = new Dep();
class Watcher{
constructor(data, key, ck){
Dep.target = this;
this.data = data;
this.key = key;
this.ck = ck;
this.init()
}
init(){
this.value = utils.getValue(this.data,this.key); //在数据中获取对应的值
console.log(this.value)
Dep.target = null;
return this.value;
}
sendVal(){
let newVal = this.init();
this.ck(newVal)
}
}
class Observer{
constructor(data){
if(!data || typeof data !== 'object'){ //判断data是否为真
return;
}
this.data = data;
this.init();
}
init(){
Object.keys(this.data).forEach(key=>{ //拿到当前数据中的key值 循环
this.observer(this.data, key, this.data[key])
})
}
observer(obj, key, value){
new Observer(obj[key]) //递归继续往里找
Object.defineProperty(obj, key, { //数据劫持
get(){ //添加数据劫持后属性的获取方法
if(Dep.target){
dep.subscription(Dep.target); //执行订阅者
}
// console.log(value)
return value;
},
set(newValue){ //添加数据劫持后属性的设置方法
if(value === newValue){
return;
}
value = newValue; //重新赋值
dep.issue(); //执行发布者
new Observer(value); //为了兼容新值为一个对象的时候,该对象的属性也得添加劫持
}
})
}
}
const utils = {
setValue(node, data, key){
node.value = this.getValue(data,key); //修改input 中的value
},
getValue(data,key){ //在数据中找到对应的值
// console.log(key)
if(key.indexOf('.') > -1){ //判断key中是否有.
let arr = key.split('.'); //以.分割成一个数组
for(let i=0; i<arr.length;i++){ //循环数组
data = data[arr[i]] //每次循环都会往里找一层,循环完了就找到需要的数据了
}
return data;
}else{
return data[key];
}
},
getContent(node, data, key){
node.textContent = this.getValue(data, key); //修改{{}}文本
},
//触发input事件,视图改变,改变数据
changeKeyVal(data, key, newVal){
if(key.indexOf('.') > -1){ //判断key中是否有.
let arr = key.split('.'); //以.分割成一个数组
for(let i=0; i<arr.length-1;i++){ //循环数组
data = data[arr[i]] //每次循环都会往里找一层,循环完了就找到需要的数据了
}
data[arr[arr.length-1]] = newVal;
}else{
data[key] = newVal
}
// console.log(data) 数据已根据试图改变
}
}
class Mvvm{
constructor({el,data}){
this.el = el;
this.data = data;
this.init(); //数据劫持
this.initDom(); //替换文本中的数据为真实数据
}
init(){
new Observer(this.data); //给当前数据集合的每一个属性添加劫持
}
initDom(){
this.$el = document.getElementById(this.el); //拿到真实DOM
let newFargment = this.createFragment();
this.compiler(newFargment);
this.$el.appendChild(newFargment);
}
createFragment(){
let newFargment = document.createDocumentFragment(); //创建一个新的空白的文档片段
let firstChild;
while(firstChild = this.$el.firstChild){ //利用while 把所有节点appendChild到一个空白文档片段中
newFargment.appendChild(firstChild);
}
return newFargment; //返回文档片段
}
compiler(node){
// console.log(node.nodeType)
if(node.nodeType === 1){ //空白节点和input的nodeType是1
let attributes = node.attributes;
Array.from(attributes).forEach(val =>{
if(val.nodeName === 'v-model'){
// console.log(val.nodeValue)
node.addEventListener('input',(e)=>{ //给input绑定input事件
// console.log(e.target.value)
utils.changeKeyVal(this.data, val.nodeValue, e.target.value)
})
utils.setValue(node, this.data, val.nodeValue);
}
})
}else if(node.nodeType === 3){ //div p...的nodeType是1
let contentVal = node.textContent.indexOf('{{') > -1 && node.textContent.split('{{')[1].split('}}')[0]; //拿到文本值 判断格式是否以{{开头,先以{{分割,截取出需要的,再以}}分割,截取出剩余的。就把{{}}去掉了。
contentVal && utils.getContent(node, this.data, contentVal); //判断contentVal是否为真,把{{val}}修改为真实数据
contentVal && new Watcher(this.data, contentVal, (newVal)=>{ //监听
node.textContent = newVal;
});
}
if(node.childNodes && node.childNodes.length > 0){ //判断子节点集合
node.childNodes.forEach(item=>{ //循环子节点集合
this.compiler(item); //递归函数
})
}
}
}