通过简单的实例解析vue双向绑定的原理。
let vm=new Vue({ el:'#app', data:{ text:'test' } })
总体实现步骤:
第一步:输入框及文本节点与data中的属性进行关联绑定
第二步:输入框内容变化时,data中的数据同步变化,即view-->model
第三步:data中的数据变化时,文本节点同步变化,即data-->view
实现一:
输入框与文本节点与data中的属性进行关联绑定,用到了es5中的Object.defineProperty方法,将data中的属性变成vm对象的访问器属性。
function defineReactive(obj,key,val){ Object.defineProperty(obj,key,{ set:function(newValue){ if(val!==newValue){ val=newValue; } }, get:function(){ return val; } }); }
完整代码:
<!DOCTYPE html> <html> <head> <title>vue</title> <style> input { border: 1px solid #ddd; padding: 8px 10px; border-radius: 5px; margin-right: 10px; } </style> </head> <body> <div id="app"> <input type="text" v-model='text'> {{text}} </div> <script type="text/javascript"> //任务分解 //1,输入框及文件节点与data中的数据绑定 //2,输入框数据变化时,data中的数据同步变化,即view=>model //3,data中的数据变化时,文本节点同步变化,即model=>view //要实现第一步,需要对dom进行编译,这里用到了documentFragment //将data中的数据设置为vm的访问器属性 function defineReactive(obj, key, val) { var dep = new Dep(); Object.defineProperty(obj, key, { set: function(newValue) { if (newValue === val) return; val = newValue; //作为发布者发出通知 dep.notify(); }, get: function() { //添加订阅者watcher到主题对象Dep if (Dep.target) { dep.addSub(Dep.target); } return val; } }); } function observe(obj, vm) { Object.keys(obj).forEach(function(key) { defineReactive(vm, key, obj[key]); }); } function Vue(options) { this.data = options.data; var data = this.data; observe(data, this); var id = options.el; var dom = nodeToFragment(document.getElementById(id), this); //编译完成将dom返回到app中 document.getElementById(id).appendChild(dom); } function nodeToFragment(node, vm) { var frag = document.createDocumentFragment(); var child; while (child = node.firstChild) { compile(child, vm); frag.appendChild(child); } return frag; } //输入框和文本节点与data进行绑定 function compile(node, vm) { var reg = /\{\{(.*)\}\}/; //节点类型为元素 if (node.nodeType == 1) { var attrs = node.attributes; for (var i = 0, len = attrs.length; i < len; i++) { if (attrs[i].nodeName == 'v-model') { //获得v-model绑定的属性名 var name = attrs[i].nodeValue; node.addEventListener('input', function() { vm[name] = this.value; }); node.value = vm[name]; node.removeAttribute('v-model'); } } new Watcher(vm, node, name, 'input'); } else if (node.nodeType == 3) { //节点类型为text if (reg.test(node.nodeValue)) { var name = RegExp.$1; //获取匹配到的字符串 name = name.trim(); // node.nodeValue = vm[name]; new Watcher(vm, node, name); } } } //订阅发布模式(观察者模式) //发布者发布消息=>主题对象收到消息并推送给订阅者=>订阅者执行相应操作 //为每个与数据相关的节点生成一个订阅者Watcher function Watcher(vm, node, name) { Dep.target = this; this.vm = vm; this.node = node; this.name = name; this.update(); Dep.target = null; } Watcher.prototype = { update: function() { this.get(); this.node.nodeValue = this.value; }, get: function() { this.value = this.vm[this.name]; } }; //主题对象 function Dep() { this.subs = []; } Dep.prototype = { notify: function() { this.subs.forEach(function(sub) { sub.update(); }); }, addSub: function(sub) { this.subs.push(sub); } }; var vm = new Vue({ el: 'app', data: { text: 'hello world', } }); </script> </body> </html>