一、原理
如果使用Object.defineProperty,实现一个最简单的双向绑定其实很简单,只需如下:
<script> var Vue = {}; Object.defineProperty(Vue,'$data',{ set(val){ document.getElementById('vue-item').innerText = val } }); document.addEventListener('keyup', function(e){ Vue.$data = e.target.value }) </script>
上面这个demo就是vue双向绑定最简化的原理。
二、替换元素
想想我们使用vue时的规则
new Vue({
el:'app',
data:{
text:'hello world'
}
});
写上页面结构:
<div id = 'app'> <input type='text' v-model='text'> {{text}} </div>
我们把Vue抽象为一个构造函数,传入这些值
function Vue(options){ this.data = options.data; this.id = options.el; getAllNode(document.getElementById(this.id), this); };
替换掉节点中所有的{{xxxx}}:
function compile (node, vm){ var reg = /{{(.*)}}/;// 匹配{{}}内任意字符 // 节点类型为元素 if (node.nodeType === 1) { var attr = node.attributes; // 解析属性 for (var i = 0; i < attr.length; i++){ if(attr[i].nodeName === 'v-model'){ var name = attr[i].nodeValue;// 获取绑定的属性的名字 node.value = vm.data[name];// 替换值 node.removeAttribute('v-model'); //移除v-model } }; } // 节点类型为text if(node.nodeType === 3) { if(reg.test(node.nodeValue)) { var name = RegExp.$1; //匹配到的第一个字符 name = name.trim(); node.nodeValue = vm.data[name]; // 将data的值赋值给node } } }; function getAllNode(node, vm){ var length = node.childNodes.length; for(var i = 0; i < length; i++){ compile(node.childNodes[i], vm) } };
这样就可以成功替换掉{{}}:
三、绑定元素
上面我们只是替换了元素,但还没有实现绑定
实现数据绑定,就要用到definedProperty的set和get方法:
首先我们要给vue的所有属性都添加set和get方法:
function Vue(){ // ***** observe(data,this) } // 遍历 function observe (obj, vm){ for(var key in obj){ active(vm, key, obj[key]); } } // 添加set和get function active (obj, key, val) { Object.defineProperty(obj, key, { get(){ return val; }, set(newVal){ if (newVal === val){ return }; val = newVal; } }); }
再来明确我们要做的事,获取输入的值,改变Vue中相应的data的值,同时改变{{}}中的值;
我们已经给data的每个属性都添加了get和set的方法,现在要做的就是如何触发它们。
触发它肯定是在赋值的时候,所以我们在有v-model属性的节点监听输入事件,同时赋值,触发set事件:
function compile (node, vm){ // *********** if (node.nodeType === 1) { var attr = node.attributes; // 解析属性 for (var i = 0; i < attr.length; i++){ if(attr[i].nodeName === 'v-model'){ var name = attr[i].nodeValue;// 获取绑定的属性的名字
// 监听input事件 node.addEventListener('input', function(e){ // 给相应的data属性赋值,触发set vm.data[name] = e.target.value }) node.value = vm.data[name];// 替换输入框的值为data中的值 node.removeAttribute('v-model'); } }; } // ************ }
我们监听了input事件,接下来要获取输入的值并同步改变文本;
我们肯定希望只希望哪里改变了就对哪里做处理就行了,所以我们引入一个简单的发布——订阅组件:
function pubsub(){ this.subs = [] } pubsub.prototype = { addSub: function(sub){ this.subs.push(sub); }, pub: function(){ this.subs.forEach(function(sub){ sub.update(); }) } }
在添加set和get的同时订阅事件:
function active (obj, key, val) { var pubsub = new pubsub(); Object.defineProperty(obj.data, key, { get(){ // 添加订阅 if(Pubsub.target){ pubsub.addSub(Pubsub.target); } return val; }, set(newVal){ if (newVal === val){ return }; val = newVal; // 发出通知 pubsub.pub(); } }); }
添加一个方法,来在pubsub发出通知时处理事件,我们命名为watcher:
function Watcher(vm, node, name){ Pubsub.target = this; this.name = name; this.node = node; this.vm = vm; this.update(); Pubsub.target = null } Watcher.prototype = { update(){ this.get(); this.node.nodeValue = this.value }, // 获取data中的属性值 get(){ this.value = this.vm[this.name] // 触发相应的get } }
function getAllNode(node, vm){ var length = node.childNodes.length; for(var i = 0; i < length; i++){ compile(node.childNodes[i], vm) } };
这个watcher我们在什么时候添加呢?当然是在一开始的时候(compile里):
function compile (node, vm){ var reg = /{{(.*)}}/;// 匹配{{}}内任意字符 // 节点类型为元素 if (node.nodeType === 1) { var attr = node.attributes; // 解析属性 for (var i = 0; i < attr.length; i++){ if(attr[i].nodeName === 'v-model'){ var name = attr[i].nodeValue;// 获取绑定的属性的名字 node.addEventListener('input', function(e){ // 给相应的data属性赋值,触发set vm.data[name] = e.target.value }) node.value = vm.data[name];// 替换输入框的值为data中的值 node.removeAttribute('v-model'); } }; }; // 节点类型为text if(node.nodeType === 3) { if(reg.test(node.nodeValue)) { var name = RegExp.$1; //匹配到的第一个字符 name = name.trim(); // node.nodeValue = vm[name]; new Watcher(vm, node, name);// 观察输入的值 } } };
至此,便模拟了整个数据绑定的流程
四、总结
最后理清整个过程的思路
创建Vue:
input事件: