MVVM的数据绑定

在这个前端大爆发的年头,追着这些框架跑,真的好无力。angular、vue、react三个框架在领跑着,angular要出2.0了,vue也要2.0了,各种框架蓄势待发,好像潮流又要改一改的节奏。。。

在这个对新手不友好的前端发展阶段,掌握一些核心才是关键。。。好的,我在扯淡,这是本人对mvvm的数据绑定的一些见解,请轻拍

angular1.x的检脏机制

先来看一些简单的例子

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
<body>
<span ng-bind = "count"></span>
<button ng-click = "add">+</button>
<button ng-click = "minus">-</button>
<script type="text/javascript">
var Scope = function(){
var scope = this
//绑定指令的函数
var bind = function(){
var click = document.querySelectorAll('[ng-click]')
click.forEach(function(item){
console.log(item.getAttribute('ng-click'))
item.onclick = (function(item){
return function(){
scope[item.getAttribute('ng-click')]()
scope.$apply()
}
})(item)
})
}
bind()
//检脏函数
scope.$apply = function () {
var span = document.querySelectorAll('[ng-bind]')
span.forEach(function(item){
if (item.innerHTML!==scope[item.getAttribute('ng-bind')]) {
item.innerHTML = scope[item.getAttribute('ng-bind')]
}
})
}
//加法
scope.add = function () {
scope.count++
}
//减法
scope.minus = function () {
scope.count--
}
//绑定的数
scope.count = 0
//初始化
scope.$apply()
}
Scope()
</script>
</body>

以上就是简单的实现
利用bind函数来将ng-click这个指令分装,在为点击事件绑定相应的方法然后调用apply进行脏检测,而apply方法检测视图的值与js变量值是否一致,不一致则对其进行视图值进行刷新。基本的实现就完成了。
在Scope函数的最后加上一个settimeout函数试试

1
2
3
4
5
6
scope.$apply() //原有的
//添加settimeout方法
setTimeout(function(){
scope.count++
console.log(scope.count)//1
},1000)

显然上面代码是不行的,虽然值改变了(控制台输出1),但是页面值没有改变,为什么这样呢?嗯,是没有去检脏导致的,所以我们需要手动得去检脏

1
2
3
4
5
6
7
scope.$apply() //原有的
//添加settimeout方法
setTimeout(function(){
scope.count++
console.log(scope.count)//1
scope.$apply()
},1000)

嗯,一切按我们所预想的实现了,页面值也在延时后变化了,这就是angular1.x的检脏机制。
原则上就是当变量改变了,就要去调用一次apply,来进行检脏。angular也为此封装了很多指令和服务,为我们自动检脏,例如ng-click,$timeout等
现在试着编写一个双向数据绑定的例子

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
<body>
<span ng-bind = "count"></span>
<button ng-click = "add">+</button>
<button ng-click = "minus">-</button>
<input ng-model = "count"/>
<script type="text/javascript">
var Scope = function(){
var scope = this
//绑定指令的函数
var bind = function(){
var click = document.querySelectorAll('[ng-click]')
click.forEach(function(item){
console.log(item.getAttribute('ng-click'))
item.onclick = (function(item){
return function(){
scope[item.getAttribute('ng-click')]()
scope.$apply()
}
})(item)
})
var input = document.querySelectorAll('[ng-model]')
input.forEach(function(item){
item.onkeyup = function(){
scope[item.getAttribute('ng-model')] = item.value
scope.$apply()
}
item.select = function(){
scope[item.getAttribute('ng-model')] = item.value
scope.$apply()
}
})
}
bind()
//检脏函数
scope.$apply = function () {
var span = document.querySelectorAll('[ng-bind]')
var input = document.querySelectorAll('[ng-model]')
span.forEach(function(item){
if (item.innerHTML!==scope[item.getAttribute('ng-bind')]) {
item.innerHTML = scope[item.getAttribute('ng-bind')]
}
})
input.forEach(function(item){
if (item.value!==scope[item.getAttribute('ng-model')]) {
item.value = scope[item.getAttribute('ng-model')]
}
})
}
//加法
scope.add = function () {
scope.count++
}
//减法
scope.minus = function () {
scope.count--
}
//绑定的数
scope.count = 0
//初始化
scope.$apply()
setTimeout(function(){
scope.count++
scope.$apply()
console.log(scope.count)
},1000)
}
Scope()
</script>
</body>

完美实现,这就是简单的双向数据绑定,利用input绑定事件来改变js的值,然后在检脏。
其实angular1.x里面的主要实现不一定和本文相符,它的双向绑定的事件也不是本文这样简单的keyup,select;
它主要是通过$compile来编译文本,把那些指令都给缓存起来, 这个模板引擎也会被编译成指令,而缓存的对象有视图值,变量值等属性,十分复杂,本文仅仅简单实现一下原理。。。

vue的劫持get和set的方法

先来说说Object.defineProperty
Object.defineProperty是ES5的一个特性,这也是为什么vue不支持IE8以下的理由。
Object.defineProperty(object, propertyname, descriptor)
object
必需。 要在其上添加或修改属性的对象。 这可能是一个本机 JavaScript 对象(即用户定义的对象或内置对象)或 DOM 对象。
propertyname
必需。 一个包含属性名称的字符串。
descriptor
必需。 属性描述符。 它可以针对数据属性或访问器属性。
来个栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var test = { key:0 };
Object.defineProperty(test, 'key', {
get: function() {
console.log('get:' + key);
//do something
return key;
},
set: function(value) {
key = value;
console.log('set:' + key);
//do something
}
});
test.key = 2; // set:2
console.log(test.key); // get:2

从上面可以看出这个,每次访问或者操作一个对象的内容时都可以触发相应的回调函数(set,get),这样实时监测对对象的操作,然后在do something执行设置好的监听函数之类的,进行数据的刷新。这样和检脏对比来说,性能可就好了很多,修改一个值,就刷新这个值所绑定的视图,而脏检测就只能遍历所有的新旧值。。。 但这仅限于对象,那在数组里面是怎样的呢,vue是直接把数组的原型给改了,对几个方法进行了封装。
请看下面这个栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
Array.prototype.push_old = Array.prototype.push
delete Array.prototype['push']
Array.prototype.push = function (data) {
this.push_old(data)
console.log('do somthing')
}
var arr = []
arr.push(2); //do something
console.log(arr) //[2]
arr.push([3]) //do something
console.log(arr) //[2,[3]]
arr[1].push(3) //do something
console.log(arr) //[2,[3,3]]

这样就简单地把它封装好了,每次push之后,都可以执行相关的函数,把相应的视图进行刷新,很赞。但是vue可能并不是这么简单的玩起来。。下面是在别人blog抄来的一个vue源码节选

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
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
/**
- Intercept mutating methods and emit events
*/
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
// cache original method
var original = arrayProto[method]
def(arrayMethods, method, function mutator () {
// avoid leaking arguments:
// http://jsperf.com/closure-with-arguments
var i = arguments.length
var args = new Array(i)
while (i--) {
args[i] = arguments[i]
}
var result = original.apply(this, args)
var ob = this.__ob__
var inserted
switch (method) {
case 'push':
inserted = args
break
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})

vue把数组的几个方法改掉,用来可以触发监听的视图改变事件。但是这样子,还有一个问题,就是数组arr[1] = 0;这样的操作vue根本没办法检测到这样的操作发生。。为此 Vue.js 在文档中明确提示不建议直接角标修改数据,但也还是提供了一个$set方法来解决这个问题;其本质就是Array原型设置一个$set方法,类似angular的apply检脏;而且需要手动触发,也就是如果写了arr[1] = 0;然后又想要视图改变的话,手动执行$set方法。。

写在最后

前端真的很有趣。。。