在这个前端大爆发的年头,追着这些框架跑,真的好无力。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(function(){ scope.count++ console.log(scope.count) },1000)
|
显然上面代码是不行的,虽然值改变了(控制台输出1),但是页面值没有改变,为什么这样呢?嗯,是没有去检脏导致的,所以我们需要手动得去检脏
1 2 3 4 5 6 7
| scope.$apply() setTimeout(function(){ scope.count++ console.log(scope.count) 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); return key; }, set: function(value) { key = value; console.log('set:' + key); } }); test.key = 2; console.log(test.key);
|
从上面可以看出这个,每次访问或者操作一个对象的内容时都可以触发相应的回调函数(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); console.log(arr) arr.push([3]) console.log(arr) arr[1].push(3) console.log(arr)
|
这样就简单地把它封装好了,每次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) { var original = arrayProto[method] def(arrayMethods, method, function mutator () { 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) ob.dep.notify() return result }) })
|
vue把数组的几个方法改掉,用来可以触发监听的视图改变事件。但是这样子,还有一个问题,就是数组arr[1] = 0;这样的操作vue根本没办法检测到这样的操作发生。。为此 Vue.js 在文档中明确提示不建议直接角标修改数据,但也还是提供了一个$set方法来解决这个问题;其本质就是Array原型设置一个$set方法,类似angular的apply检脏;而且需要手动触发,也就是如果写了arr[1] = 0;然后又想要视图改变的话,手动执行$set方法。。
写在最后
前端真的很有趣。。。