一、原理 如果使用 Object.defineProperty,实现一个最简单的双向绑定其实很简单,只需如下:
1 2 3 4 5 6 7 8 9 var Vue = {}; Object.defineProperty(Vue,'$data',{ set(val){ document.getElementById('vue-item').innerText = val } }); document.addEventListener('keyup', function(e){ Vue.$data = e.target.value })
上面这个 demo 就是 vue 双向绑定最简化的原理。
二、替换元素 想想我们使用 vue 时的规则
1 2 3 4 5 6 new Vue({ el:'#app', data:{ text:'hello world' } });
写上页面结构:
1 2 3 4 <div id = 'app'> <input type='text' v-model='text'> text(markedown不让有,假设text‘双中括号’在里面) </div>
我们把 Vue 抽象为一个构造函数,传入这些值
1 2 3 4 5 function Vue(options){ this.data = options.data; this.id = options.el; getAllNode(document.getElementById(this.id), this); };
替换掉节点中所有的双中括号里的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 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 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 事件,接下来要获取输入的值并同步改变文本;
我们肯定希望只希望哪里改变了就对哪里做处理就行了,所以我们引入一个简单的发布——订阅组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function pubsub(){ this.subs = [] } pubsub.prototype = { addSub: function(sub){ this.subs.push(sub); }, pub: function(){ this.subs.forEach(function(sub){ sub.update(); }) } }
在添加 set 和 get 的同时订阅事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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 里):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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 事件:
源码在我的 github 仓库:https://github.com/lastnigtic/vue-bindData