1、原理

Vue的双向数据绑定的原理相信大家也都十分了解了,主要是通过​​ Object对象的defineProperty属性,重写data的set和get函数来实现的​​,这里对原理不做过多描述,主要还是来实现一个实例。为了使代码更加的清晰,这里只会实现最基本的内容,主要实现v-model,v-bind 和v-click三个命令,其他命令也可以自行补充。

添加网上的一张图

Vue的双向数据绑定_数据绑定

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

包含:

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三个指令。效果如下图

Vue的双向数据绑定_作用域_02

附上全部代码,不到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