1、原理
Vue的双向数据绑定的原理相信大家也都十分了解了,主要是通过 Object对象的defineProperty属性,重写data的set和get函数来实现的
,这里对原理不做过多描述,主要还是来实现一个实例。为了使代码更加的清晰,这里只会实现最基本的内容,主要实现v-model,v-bind 和v-click三个命令,其他命令也可以自行补充。
添加网上的一张图
2、实现
页面结构很简单,如下
1 <div id="app"> 2 <form> 3 <input type="text" v-model="number"> 4 <button type="button" v-click="increment">增加</button> 5 </form> 6 <h3 v-bind="number"></h3> 7 </div>
包含:
1. 一个input,使用v-model指令
2. 一个button,使用v-click指令
3. 一个h3,使用v-bind指令。
我们最后会通过类似于vue的方式来使用我们的双向数据绑定,结合我们的数据结构添加注释
1 var app = new myVue({ 2 el:'#app', 3 data: { 4 number: 0 5 }, 6 methods: { 7 increment: function() { 8 this.number ++; 9 }, 10 } 11 })
首先我们需要定义一个myVue构造函数:
1 function myVue(options) { 2 3 }
为了初始化这个构造函数,给它添加一 个_init属性
1 function myVue(options) { 2 this._init(options); 3 } 4 myVue.prototype._init = function (options) { 5 this.$options = options; // options 为上面使用时传入的结构体,包括el,data,methods 6 this.$el = document.querySelector(options.el); // el是 #app, this.$el是id为app的Element元素 7 this.$data = options.data; // this.$data = {number: 0} 8 this.$methods = options.methods; // this.$methods = {increment: function(){}} 9 }
接下来实现_obverse函数,对data进行处理,重写data的set和get函数
并改造_init函数
1 myVue.prototype._obverse = function (obj) { // obj = {number: 0} 2 var value; 3 for (key in obj) { //遍历obj对象 4 if (obj.hasOwnProperty(key)) { 5 value = obj[key]; 6 if (typeof value === 'object') { //如果值还是对象,则遍历处理 7 this._obverse(value); 8 } 9 Object.defineProperty(this.$data, key, { //关键 10 enumerable: true, 11 configurable: true, 12 get: function () { 13 console.log(`获取${value}`); 14 return value; 15 }, 16 set: function (newVal) { 17 console.log(`更新${newVal}`); 18 if (value !== newVal) { 19 value = newVal; 20 } 21 } 22 }) 23 } 24 } 25 } 26 27 myVue.prototype._init = function (options) { 28 this.$options = options; 29 this.$el = document.querySelector(options.el); 30 this.$data = options.data; 31 this.$methods = options.methods; 32 33 this._obverse(this.$data); 34 }
接下来我们写一个指令类Watcher,用来绑定更新函数,实现对DOM元素的更新
1 function Watcher(name, el, vm, exp, attr) { 2 this.name = name; //指令名称,例如文本节点,该值设为"text" 3 this.el = el; //指令对应的DOM元素 4 this.vm = vm; //指令所属myVue实例 5 this.exp = exp; //指令对应的值,本例如"number" 6 this.attr = attr; //绑定的属性值,本例为"innerHTML" 7 8 this.update(); 9 } 10 11 Watcher.prototype.update = function () { 12 this.el[this.attr] = this.vm.$data[this.exp]; //比如 H3.innerHTML = this.data.number; 当number改变时,会触发这个update函数,保证对应的DOM内容进行了更新。 13 }
更新_init函数以及_obverse函数
1 myVue.prototype._init = function (options) { 2 //... 3 this._binding = {}; //_binding保存着model与view的映射关系,也就是我们前面定义的Watcher的实例。当model改变时,我们会触发其中的指令类更新,保证view也能实时更新 4 //... 5 } 6 7 myVue.prototype._obverse = function (obj) { 8 //... 9 if (obj.hasOwnProperty(key)) { 10 this._binding[key] = { // 按照前面的数据,_binding = {number: _directives: []} 11 _directives: [] 12 }; 13 //... 14 var binding = this._binding[key]; 15 Object.defineProperty(this.$data, key, { 16 //... 17 set: function (newVal) { 18 console.log(`更新${newVal}`); 19 if (value !== newVal) { 20 value = newVal; 21 binding._directives.forEach(function (item) { // 当number改变时,触发_binding[number]._directives 中的绑定的Watcher类的更新 22 item.update(); 23 }) 24 } 25 } 26 }) 27 } 28 } 29 }
那么如何将view与model进行绑定呢?接下来我们定义一个_compile函数,用来解析我们的指令(v-bind,v-model,v-clickde)等,并在这个过程中对view与model进行绑定。
1 myVue.prototype._init = function (options) { 2 //... 3 this._complie(this.$el); 4 } 5 6 myVue.prototype._complie = function (root) { root 为 id为app的Element元素,也就是我们的根元素 7 var _this = this; 8 var nodes = root.children; 9 for (var i = 0; i < nodes.length; i++) { 10 var node = nodes[i]; 11 if (node.children.length) { // 对所有元素进行遍历,并进行处理 12 this._complie(node); 13 } 14 15 if (node.hasAttribute('v-click')) { // 如果有v-click属性,我们监听它的onclick事件,触发increment事件,即number++ 16 node.onclick = (function () { 17 var attrVal = nodes[i].getAttribute('v-click'); 18 return _this.$methods[attrVal].bind(_this.$data); //bind是使data的作用域与method函数的作用域保持一致 19 })(); 20 } 21 22 if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) { // 如果有v-model属性,并且元素是INPUT或者TEXTAREA,我们监听它的input事件 23 node.addEventListener('input', (function(key) { 24 var attrVal = node.getAttribute('v-model'); 25 //_this._binding['number']._directives = [一个Watcher实例] 26 // 其中Watcher.prototype.update = function () { 27 // node['vaule'] = _this.$data['number']; 这就将node的值保持与number一致 28 // } 29 _this._binding[attrVal]._directives.push(new Watcher( 30 'input', 31 node, 32 _this, 33 attrVal, 34 'value' 35 )) 36 37 return function() { 38 _this.$data[attrVal] = nodes[key].value; // 使number 的值与 node的value保持一致,已经实现了双向绑定 39 } 40 })(i)); 41 } 42 43 if (node.hasAttribute('v-bind')) { // 如果有v-bind属性,我们只要使node的值及时更新为data中number的值即可 44 var attrVal = node.getAttribute('v-bind'); 45 _this._binding[attrVal]._directives.push(new Watcher( 46 'text', 47 node, 48 _this, 49 attrVal, 50 'innerHTML' 51 )) 52 } 53 } 54 }
至此,我们已经实现了一个简单vue的双向绑定功能,包括v-bind, v-model, v-click三个指令。效果如下图
附上全部代码,不到150行
1 <!DOCTYPE html> 2 <head> 3 <title>myVue</title> 4 </head> 5 <style> 6 #app { 7 text-align: center; 8 } 9 </style> 10 <body> 11 <div id="app"> 12 <form> 13 <input type="text" v-model="number"> 14 <button type="button" v-click="increment">增加</button> 15 </form> 16 <h3 v-bind="number"></h3> 17 </div> 18 </body> 19 20 <script> 21 function myVue(options) { 22 this._init(options); 23 } 24 25 myVue.prototype._init = function (options) { 26 this.$options = options; 27 this.$el = document.querySelector(options.el); 28 this.$data = options.data; 29 this.$methods = options.methods; 30 31 this._binding = {}; 32 this._obverse(this.$data); 33 this._complie(this.$el); 34 } 35 36 myVue.prototype._obverse = function (obj) { 37 var value; 38 for (key in obj) { 39 if (obj.hasOwnProperty(key)) { 40 this._binding[key] = { 41 _directives: [] 42 }; 43 value = obj[key]; 44 if (typeof value === 'object') { 45 this._obverse(value); 46 } 47 var binding = this._binding[key]; 48 Object.defineProperty(this.$data, key, { 49 enumerable: true, 50 configurable: true, 51 get: function () { 52 console.log(`获取${value}`); 53 return value; 54 }, 55 set: function (newVal) { 56 console.log(`更新${newVal}`); 57 if (value !== newVal) { 58 value = newVal; 59 binding._directives.forEach(function (item) { 60 item.update(); 61 }) 62 } 63 } 64 }) 65 } 66 } 67 } 68 69 myVue.prototype._complie = function (root) { 70 var _this = this; 71 var nodes = root.children; 72 for (var i = 0; i < nodes.length; i++) { 73 var node = nodes[i]; 74 if (node.children.length) { 75 this._complie(node); 76 } 77 78 if (node.hasAttribute('v-click')) { 79 node.onclick = (function () { 80 var attrVal = nodes[i].getAttribute('v-click'); 81 return _this.$methods[attrVal].bind(_this.$data); 82 })(); 83 } 84 85 if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) { 86 node.addEventListener('input', (function(key) { 87 var attrVal = node.getAttribute('v-model'); 88 _this._binding[attrVal]._directives.push(new Watcher( 89 'input', 90 node, 91 _this, 92 attrVal, 93 'value' 94 )) 95 96 return function() { 97 _this.$data[attrVal] = nodes[key].value; 98 } 99 })(i)); 100 } 101 102 if (node.hasAttribute('v-bind')) { 103 var attrVal = node.getAttribute('v-bind'); 104 _this._binding[attrVal]._directives.push(new Watcher( 105 'text', 106 node, 107 _this, 108 attrVal, 109 'innerHTML' 110 )) 111 } 112 } 113 } 114 115 function Watcher(name, el, vm, exp, attr) { 116 this.name = name; //指令名称,例如文本节点,该值设为"text" 117 this.el = el; //指令对应的DOM元素 118 this.vm = vm; //指令所属myVue实例 119 this.exp = exp; //指令对应的值,本例如"number" 120 this.attr = attr; //绑定的属性值,本例为"innerHTML" 121 122 this.update(); 123 } 124 125 Watcher.prototype.update = function () { 126 this.el[this.attr] = this.vm.$data[this.exp]; 127 } 128 129 window.onload = function() { 130 var app = new myVue({ 131 el:'#app', 132 data: { 133 number: 0 134 }, 135 methods: { 136 increment: function() { 137 this.number ++; 138 }, 139 } 140 }) 141 } 142 </script>
转自https://segmentfault.com/a/1190000014274840