Vue中的双向数据绑定

1. 原理

Vue的双线数据绑定的原理主要是通过Object对象的defineProperty属性,重写datasetget函数来实现的,这里通过一个实例来说明。主要实现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>

包含:

  1. 一个input,使用v-model指令
  2. 一个button,使用v-click指令
  3. 一个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>
文章作者: Monad Kai
文章链接: onlookerliu.github.io/2018/05/03/Vue中的双向数据绑定/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Code@浮生记
支付宝打赏
微信打赏