实现Vue的双向绑定
一、概述
之前有讲到过vue实现整体的整体流程,讲到过数据的响应式,是通过Object.defineProperity来实现的,当时只是举了一个小小的例子,那么再真正的vue框架里是如何实现数据的双向绑定呢?是如何将vm.data中的属性通过“v-model”和“{{}}”绑定到页面上的呢?下面我们先抛弃vue中DOM渲染的机制,自己来动手实现一双向绑定的demo。
二、实现步骤
1、html部分
根据Vue的语法,定义html需要绑定的DOM,如下代码
2、js部分
由于直接操作DOM是非常损耗性能的,所以这里我们使用DocumentFragment(以下简称为文档片段),由于createDocumentFragment是在内存中创建的一个虚拟节点对象,所以往文档片段里添加DOM节点是不太消耗性能的;此处我们将app下面的节点都劫持到文档片段中,在文档片段中对DOM进行一些操作,然后将文档片段总体重新插入app容器里面去,而且此处插入到app中的节点都是属于文档片段的子孙节点。代码如下:
1 // 劫持DOM节点到DocumentFragment中 2 function nodeToFragment(node) { 3 var flag = document.createDocumentFragment(); 4 while(node.firstChild) { 5 flag.appendChild(node.firstChild) // 劫持节点到文档片段中,在此之前对节点进行一些操作; 劫持到一个,对应的DOM容器里会删除掉一个节点 6 } 7 return flag 8 }; 9 var dom = nodeToFragment(document.getElementById('app')) 10 document.getElementById('app').apendChild(dom) // 将文档片段重新放入app中
对于双向绑定的实现,首先我们来创建vue的实例
1 // 创建Vue对象 2 function Vue(data) { 3 var id = data.el; 4 var ele = document.getElementById(id); 5 this.data = data.data; 6 obersve(this.data, this) // 将vm.data指向vm 7 var dom = nodeToFragment(ele, this); // 通过上面的函数劫持DOM节点 8 ele.appendChild(dom); // 将文档片段重新放入容器 9 }; 10 // 实例化Vue对象 11 var vm = new Vue({ 12 el: 'app', 13 data: { 14 text: 'hello world' 15 } 16 })
通过以上代码我们可以看到,实例化Vue对象的时候先是将vm.data指向到了vm,而后是对html节点进行的数据绑定,此处分两步,我们先来看对vm的数据源绑定:
1 function definevm(vm, key, value) { 2 Object.defineProperty(vm, key, { 3 get: function() { 4 return value 5 }, 6 set: function(newval) { 7 value = newval 8 console.log(value) 9 } 10 }) 11 }; 12 // 指定data到vm 13 function obersve(data, vm) { 14 for(var key in data) { 15 definevm(vm, key, data[key]); 16 } 17 } 18 19 vm.text = 'MrGao'; 20 console.log(vm.text); // MrGao
此处将vm.data的属性指定到vm上,并且实现了对vm的监听,一旦vm的属性发生变化,便会触发其set方法;接下来我们来看下对DOM节点的数据绑定:
1 // 绑定数据 2 function compile(node, vm) { 3 // console.log(node.nodeName) 4 var reg = /\{\{(.*)\}\}/; // 匹配{{}}里的内容 5 if (node.nodeType === 1) { // 普通DOM节点nodeType为1 6 var attr = node.attributes 遍历节点属性 7 for(var i = 0; i < attr.length; i++) { 8 if (attr[i].nodeName === 'v-model') { 9 var name = attr[i].nodeValue; // 获取绑定的值 10 node.addEventListener('keyup', function(e) { 11 // console.log(e.target.value) 12 vm[name] = e.target.value //监听input值的变化,重新给vm.text赋值 13 }) 14 node.value = vm[name]; 15 node.removeAttribute('v-model'); 16 }; 17 }; 18 }; 19 if (node.nodeType === 3) { 20 if (reg.test(node.nodeValue)) { 21 var name = RegExp.$1; 22 name = name.trim(); 23 node.nodeValue = vm[name]; // 将vm.text的值赋给文本节点 24 } 25 } 26 } 27 // 劫持DOM节点到DocumentFragment中 28 function nodeToFragment(node, vm) { 29 var flag = document.createDocumentFragment(); 30 while(node.firstChild) { 31 compile(node.firstChild, vm); // 进行数据绑定 32 flag.appendChild(node.firstChild); // 劫持节点到文档片段中 33 } 34 return flag; 35 };
这样一来,我们就可以通过compile方法将vm.text绑定到input节点和下面的文本节点上,并且监听input节点的keyup事件,当input的value发生改变是,将input的值赋给vm.text,如此vm.text的值也改变了,同时会触发对vm的ste函数;但是vm.text的值是改变了,我们应该如何让文本节点的值同样跟随者vm.text的值改变呢?此时我们就可以使用订阅模式(观察者模式)来实现这一功能;那什么是订阅模式呢?
订阅模式就是好比有一家报社,他每天都要对新的世界大事进行发布,然后报社通知送报员去把发布的新的报纸推送给订阅者,订阅这在拿到报纸后可以获取到新的消息;反映到代码里可以这样理解;当vm.text改变时,触发set方法,然后发布变化的消息,在数据绑定的那里定义订阅者,在定义一个连接两者的“送报员”,每当发布者发布新的消息,订阅者都可以拿到新的消息,代码如下:
1 // 定义发布订阅 2 function Dep() { 3 this.subs = [] 4 } 5 Dep.prototype = { 6 addSub: function(sub) { 7 this.subs.push(sub); 8 }, 9 notify: function() { 10 this.subs.forEach(function(sub) { 11 sub.update(); 12 }) 13 } 14 }; 15 // 定义观察者 16 function Watcher (vm, node, name) { 17 Dep.target = this; // 发布者和订阅者的桥梁(送报员) 18 this.name = name; 19 this.node = node; 20 this.vm = vm; 21 this.update(); 22 Dep.target = null; 23 }; 24 Watcher.prototype = { 25 update: function() { 26 this.get(); 27 // console.log(this.node.nodeName) 28 if (this.node.nodeName === 'INPUT') { 29 this.node.value = this.value; 30 } else { 31 this.node.nodeValue = this.value; 32 } 33 }, 34 get: function() { 35 this.value = this.vm[this.name]; 36 } 37 }
此时,发布者和订阅者要分别在数据更新时和数据绑定时进行绑定
1 // 绑定发布者 2 function definevm(vm, key, value) { 3 var dep = new Dep // 实例化发布者 4 Object.defineProperty(vm, key, { 5 get: function() { 6 if (Dep.target) { 7 dep.addSub(Dep.target) // 为每个属性绑定watcher 8 } 9 return value 10 }, 11 set: function(newval) { 12 value = newval 13 console.log(value) 14 dep.notify(); // 数据改变执行发布 15 } 16 }) 17 }; 18 19 // 绑定订阅者到节点上面 20 function compile(node, vm) { 21 // console.log(node.nodeName) 22 var reg = /\{\{(.*)\}\}/; 23 if (node.nodeType === 1) { 24 var attr = node.attributes 25 for(var i = 0; i < attr.length; i++) { 26 if (attr[i].nodeName === 'v-model') { 27 var name = attr[i].nodeValue; 28 node.addEventListener('keyup', function(e) { 29 // console.log(e.target.value) 30 vm[name] = e.target.value 31 }) 32 // node.value = vm[name]; 33 new Watcher(vm, node, name); // 初始化绑定input节点 34 node.removeAttribute('v-model'); 35 }; 36 }; 37 }; 38 if (node.nodeType === 3) { 39 if (reg.test(node.nodeValue)) { 40 var name = RegExp.$1; 41 name = name.trim(); 42 // node.nodeValue = vm[name]; 43 new Watcher(vm, node, name); // 文本节点绑定订阅者 44 } 45 } 46 }
到这里vue的双绑定就实现了,此文仅为实现最简单的双向绑定,一些其它复杂的条件都没有考虑在内,为理想状态下,如有纰漏还望指正,下面附上完整代码
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Vue</title> 6 </head> 7 <body> 8 <div id="app"> 9 <input type="text" id="a" v-model="text"> 10 {{text}} 11 </div> 12 </body> 13 <script> 14 // 定义发布订阅 15 function Dep() { 16 this.subs = [] 17 } 18 Dep.prototype = { 19 addSub: function(sub) { 20 this.subs.push(sub); 21 }, 22 notify: function() { 23 this.subs.forEach(function(sub) { 24 sub.update(); 25 }) 26 } 27 }; 28 // 定义观察者 29 function Watcher (vm, node, name) { 30 Dep.target = this; 31 this.name = name; 32 this.node = node; 33 this.vm = vm; 34 this.update(); 35 Dep.target = null; 36 }; 37 Watcher.prototype = { 38 update: function() { 39 this.get(); 40 // console.log(this.node.nodeName) 41 if (this.node.nodeName === 'INPUT') { 42 this.node.value = this.value; 43 } else { 44 this.node.nodeValue = this.value; 45 } 46 }, 47 get: function() { 48 this.value = this.vm[this.name]; 49 } 50 } 51 // 绑定数据 52 function compile(node, vm) { 53 // console.log(node.nodeName) 54 var reg = /\{\{(.*)\}\}/; 55 if (node.nodeType === 1) { 56 var attr = node.attributes 57 for(var i = 0; i < attr.length; i++) { 58 if (attr[i].nodeName === 'v-model') { 59 var name = attr[i].nodeValue; 60 node.addEventListener('keyup', function(e) { 61 // console.log(e.target.value) 62 vm[name] = e.target.value 63 }) 64 // node.value = vm[name]; 65 new Watcher(vm, node, name); 66 node.removeAttribute('v-model'); 67 }; 68 }; 69 }; 70 if (node.nodeType === 3) { 71 if (reg.test(node.nodeValue)) { 72 var name = RegExp.$1; 73 name = name.trim(); 74 // node.nodeValue = vm[name]; 75 new Watcher(vm, node, name); 76 } 77 } 78 } 79 // 劫持DOM节点到DocumentFragment中 80 function nodeToFragment(node, vm) { 81 var flag = document.createDocumentFragment(); 82 while(node.firstChild) { 83 // console.log(node.firstChild) 84 compile(node.firstChild, vm) 85 flag.appendChild(node.firstChild) // 劫持节点到文档片段中 86 } 87 return flag 88 }; 89 function definevm(vm, key, value) { 90 var dep = new Dep 91 Object.defineProperty(vm, key, { 92 get: function() { 93 if (Dep.target) { 94 dep.addSub(Dep.target) 95 } 96 return value 97 }, 98 set: function(newval) { 99 value = newval 100 console.log(value) 101 dep.notify(); 102 } 103 }) 104 }; 105 // 指定data到vm 106 function obersve(data, vm) { 107 for(var key in data) { 108 definevm(vm, key, data[key]); 109 } 110 } 111 // 创建Vue类 112 function Vue (options) { 113 this.data = options.data; 114 var id = options.el; 115 var ele = document.getElementById(id); 116 117 // 将data的数据指向vm 118 obersve(this.data, this); 119 // 存DOM到文档片段 120 var dom = nodeToFragment(ele, this); 121 // 编译完成将DOM返回挂在容器中 122 ele.appendChild(dom); 123 }; 124 // 创建Vue实例 125 var vm = new Vue({ 126 el: 'app', 127 data: { 128 text: 'hello world' 129 } 130 }) 131 </script> 132 </html>
参考文章:https://www.cnblogs.com/kidney/p/6052935.html?utm_source=gold_browser_extension