1. 原理
Vue的双线数据绑定的原理主要是通过Object
对象的defineProperty
属性,重写data
的set
和get
函数来实现的,这里通过一个实例来说明。主要实现v-model,v-bind和v-click三个命令,其他命令也可以自行补充。
添加网上的一张图
2. 实现
页面结构很简单,如下
1 2 3 4 5 6
| <div id="app"> <form> <input type="text" v-model="number"> <button type="button" v-click="increment">增加</button> </form> </div>
|
包含:
- 一个input,使用v-model指令
- 一个button,使用v-click指令
- 一个h3,使用v-bind指令
我们最后会通过类似与Vue的方式来使用我们的双向数据绑定,结合我们的数据结构添加注释:
1 2 3 4 5 6 7 8 9 10 11
| var app = new myVue({ el: '#app', data: { number: 0 }, methods: { increment: function() { this.number++; }, } })
|
首先我们需要定义一个myVue构造函数
1 2 3
| function myVue(options) {
}
|
为了初始化这个构造函数,给它添加一个_init
属性:
1 2 3 4 5 6 7 8 9
| function myVue(options) { this._init(options); } myVue.prototype._init = function (options) { this.$options = options; this.$el = document.querySelector(options.el); this.$data = options.data; this.$methods = options.methods; }
|
接下来实现 _obverse
函数,对data进行处理,重写data的set和get函数,并改造_init函数:
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 33
| myVue.prototype._obverse = function(obj) { var value; for (key in obj) { if (obj.hasOwnProperty(key)) { value = obj[key]; if (typeof value === 'object') { this._obverse(value); } Object.defineProperty(this.$data, key, { enumerable: true, configurable: true, get: function() { console.log(`获取${value}`); return value; }, set: function(newValue) { console.log(`更新${newValue}`); if (value !== newValue) { value = newValue; } } }) } } }
myVue.prototype._init = function(options) { this.$options = options; this.$el = document.querySelector(options.el); this.$data = options.data; this.$methods = options.methods; this._obverse(this.$data); }
|
接下来我们写一个指令类Watcher,用来绑定更新函数,实现对DOM元素的更新:
1 2 3 4 5 6 7 8 9 10 11 12 13
| function Watcher(name, el, vm, exp, attr) { this.name = name; this.el = el; this.vm = vm; this.exp = exp; this.attr = attr;
this.update(); }
Watcher.prototype.update = function() { this.el[this.attr] = this.vm.$data[this.exp]; }
|
更新_init
函数以及_obverse
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| myVue.prototype._init = function(options) { this._binding = {}; }
myVue.prototype._obverse = function(obj) { if (obj.hasOwnProperty(key)) { this._binding[key] = { _directives: [] };
var binding = this._binding[key]; Object.defineProperty(this.$data, key, { set: function(newVal) { console.log(`更新${newVal}`); if (value !== newVal) { value = newVal; binding._directives.forEach(function(item) { item.update(); }) } } }) } }
|
那么如何将view与model进行绑定呢?接下来我们定义一个_compile
函数,用来解析我们的指令(v-bind, v-model, v-clicked)等,并在这个过程中对view与model进行绑定。
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 33 34 35 36 37 38 39 40 41 42 43 44 45
| myVue.prototype._init = function(options) { this._compile(this.$el); }
myVue.prototype._compile = function(root) { var _this = this; var nodes = root.children; for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; if (node.children.length) { this._compile(node); } if (node.hasAttribute('v-click')) { node.onclick = (function() { var attrVal = nodes[i].getAttribute('v-click'); return _this.$methods[attrVal].bind(_this.$data); })(); } if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) { node.addEventListener('input', (function(key) { var attrVal = node.getAttribute('v-model'); _this._binding[attrVal]._directives.push(new Watcher( 'input', node, _this, attrVal, 'value' )) return function() { _this.$data[attrVal] = nodes[key].value; } })(i)); } if (node.hasAttribute('v-bind')) { var attrVal = node.getAttribute('v-bind'); _this._binding[attrVal]._directives.push(new Watcher( 'text', node, _this, attrVal, 'innerHTML' )) } } }
|
至此,我们已经实现了一个简单vue的双向绑定功能,包括v-bind,v-model,v-click三个指令。效果如下
附上全部代码,不到150行
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
| <!DOCTYPE html> <head> <title>myVue</title> </head> <style> #app { text-align: center; } </style> <body> <div id="app"> <form> <input type="text" v-model="number"> <button type="button" v-click="increment">增加</button> </form> <h3 v-bind="number"></h3> </div> </body>
<script> function myVue(options) { this._init(options); } myVue.prototype._init = function(options) { this.$options = options; this.$el = document.querySelector(options.el); this.$data = options.data; this.$methods = options.methods; this._binding = {}; this._obverse(this.$data); this._compile(this.$el); } myVue.prototype._obverse = function(obj) { var value; for (key in obj) { if (obj.hasOwnProperty(key)) { this._binding[key] = { _directives: [] }; value = obj[key]; if (typeof value === 'object') { this._obverse(value); } var binding = this._binding[key]; Object.defineProperty(this.$data, key, { enumerable: true, configurable: true, get: function() { console.log(`获取${value}`); return value; }, set: function(newVal) { console.log(`更新${newVal}`); if (value !== newVal) { value = newVal; binding._directives.forEach(function(item) { item.update(); }) } } }) } } } myVue.prototype._compile = function(root) { var _this = this; var nodes = root.children; for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; if (node.children.length) { this._comiple(node); } if (node.hasAttribute('v-click')) { node.onclick = (function() { var attrVal = nodes[i].getAttribute('v-click'); return _this.$methods[attrVal].bind(_this.$data); })(); } if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) { node.addEventListener('input', (function(key) { var attrVal = node.getAttribute('v-model'); _this._binding[attrVal]._directives.push(new Watcher( 'input', node, _this, attrVal, 'value' )) return function() { _this.$data[attrVal] = nodes[key].value; } })(i)); } if (node.hasAttribute('v-bind')) { var attrVal = node.getAttribute('v-bind'); _this._binding[attrVal]._directives.push(new Watcher( 'text', node, _this, attrVal, 'innerHTML' )) } } } function Watcher(name, el, vm, exp, attr) { this.name = name; this.el = el; this.vm = vm; this.exp = exp; this.attr = attr; this.update(); }
Watcher.prototype.update = function() { this.el[this.attr] = this.vm.$data[this.exp]; }
window.onload = function() { var app = new myVue({ el: '#app', data: { number: 0 }, methods: { increment: function() { this.number++; }, } }) } </script>
|