内容简介:MVVM 由以下三个内容组成:在 JQuery 时期,如果需要刷新 UI 时,需要先取到对应的 DOM 再更新 UI,这样数据和业务的逻辑就和页面有强耦合。在 MVVM 中,UI 是通过
MVVM 由以下三个内容组成:
- View:界面
- Model:数据模型
- ViewModel:作为桥梁负责沟通 View 和 Model
在 JQuery 时期,如果需要刷新 UI 时,需要先取到对应的 DOM 再更新 UI,这样数据和业务的逻辑就和页面有强耦合。
在 MVVM 中,UI 是通过 数据驱动 的,数据一旦改变就会相应的刷新对应的 UI ,UI 如果改变,也会改变对应的数据。这种方式就可以在业务处理中只关心数据的流转,而无需直接和页面打交道。ViewModel 只关心数据和业务的处理,不关心 View 如何处理数据,在这种情况下,View 和 Model 都可以独立出来,任何一方改变了也不一定需要改变另一方,并且可以将一些可复用的逻辑放在一个 ViewModel 中,让多个 View 复用这个 ViewModel。
在 MVVM 中,最核心的也就是数据双向绑定,例如 Angluar 的脏数据检查,Vue 中的数据劫持。
本文主要学习总结了Angluar 的脏数据检查。
2. 脏检查机制
双向数据绑定是 AngularJS 的核心机制之一。当 view 中有任何数据变化时,会更新到 model ,当 model 中数据有变化时,view 也会同步更新,显然,这需要一个监控。
原理就是,Angular 在 scope 模型上设置了一个 监听队列 ,用来 监听数据变化 并 更新 view 。每次绑定一个东西到 view 上时 AngularJS 就会往 $watch 队列里插入 一条 $watch ,用来检测它监视的 model 里是否有变化的东西。当浏览器接收到可以被 angular context 处理的事件时, $digest 循环就会触发,遍历所有的 $watch,最后更新 dom。
2.1 $scope
Scopes are used for many different purposes:
1. Sharing data between controllers and views 2. Sharing data between different parts of the application 3. Broadcasting and listening for events 4. Watching for changes in data
可以看到,scope 才是双向数据绑定的核心部分,其中主要包含watch和digest的代码:
function Scope() { this.$$watchers = []; this.$$lastDirtyWatch = null; }
Scope类中包含一个$$watchers对象数组,该数组用于保存各数据变量的监听器。(在Angular框架中,双美元符前缀$$表示这个变量被当作私有的来考虑,不应当在外部代码中调用。)
$watch 方法用于创建监听器并绑定至当前作用域,接受两个函数做参数,把他们存储在 watchers 数组中。
scope.prototype.$watch = function(name, getNewValue, listener){ var watch = { name: name, getNewValue: getNewValue, listener : listener || function(){}, last: '', }; this.$$watchList.push(watch); }
getNewValue 函数每次放回最新值。
2.1.2 $scope
下面是$digest 函数,它执行了所有在作用域上注册的监听器,对监视器的新旧值进行对比,当新旧值不同时,调用listener函数进行相应操作,并将旧值更新为新值。它将不断重复这一过程,直到所有数据变量的新旧值相等:
Scope.prototype.$digest = function() { var dirty = true; while(dirty){ dirty = false; for(var i = 0; i < this.$$watchers.length; i++){ var newVal = this.$$watchers[i].getNewValue(); var oldVal = this.$$watchers[i].last; if(newVal! == oldVal){ dirty = true; this.$$watchers[i].listener(oldVal,newVal); this.$$watchers[i].last = newVal; } } } };
对于每一个watch,我们使用 getNewValue() 并且把scope实例传递进去,得到数据最新值 。然后和上一次值进行比较,如果不同,那就调用 listener,同时把新值和旧值一并传递进去。 最终,我们把last 属性设置为新返回的值,也就是最新值。
这个$digest 再一次调用,last 为undefined,所以一定会进行一次数据呈现。
$digest 循环的上限是 10 次 (超过 10次后抛出一个异常,防止无限循环)。
$digest 循环不会只运行一次。在当前的一次循环结束后,它会再执行一次循环用来检查是否有 models 发生了变化。
这就是脏检查(Dirty Checking),它用来处理在 listener 函数被执行时可能引起的 model 变化。因此 $digest 循环会持续运行直到 model 不再发生变化,或者 $digest 循环的次数达到了 10 次(超过 10 次后抛出一个异常,防止无限循环)。
当 $digest 循环结束时,DOM 相应地变化。
$eval、$apply和$evalAsync
$eval
$eval的作用是在scope中执行给出的表达式。
Scope.prototype.$eval = function(expr, locals) { return expr(this, locals); };
$apply
$apply作用是将外部js代码引入到scope的digest环节来。这个方法可能是非常广为人知的一个方法。尤其是用jquery处理数据更新数据,ajax获取数据更新view什么的。
它实际上调用了$.eval,然后手动触发了digest,代码:
Scope.prototype.$apply = function(expr) { try { return this.$eval(expr); } catch (e) { $exceptionHandler(e); } finally { this.$digest(); } };
finally 在 try 和 catch 代码执行完毕后执行,不管这两个环节结果如何。
实际上,AngularJS 并不直接调用 $digest(),而是调用 $scope.$apply(),后者会调用 $digest()。因此,一轮 $digest 循环在 $rootScope开始,随后会访问到所有的 children scope 中的 watchers。
现在,假设你将 ng-click 指令关联到了一个 button 上,并传入了一个function 到 ng-click 上。当该button被点击时,AngularJS 会将此 function包装到一个 wrapping function 中,然后传入到 $scope.$apply()。因此,你的function会正常被执行,修改models(如果需要的话),此时一轮$digest循环也会被触发,用来确保view也会被更新。
Note: $scope.$apply()会自动地调用$rootScope.$digest()。$apply()方法有两种形式。第一种会接受一个function作为参数,执行该function并且触发一轮$digest循环。第二种会不接受任何参数,只是触发一轮$digest循环。
$evalAsync
$evalAsync作用是代码延迟执行。setTimeout(function(){},0)是代码延迟执行其中一个办法。但是setTimeout的问题是一旦你使用了它,那么就等于完全放弃了对代码执行时机的控制——浏览器可能去渲染UI,可能去响应事件,直到很久以后才会执行指定的代码片段。$evalAsync更优于setTimeout,就是因为它在这个时机上控制得更好。
利用以上三个方法,可以将控制$digest 循环的代码优化如下:
cope.prototype.$digest = function () { var ttl = 10; var dirty; this.$$lastDirtyWatch = null; do { while (this.$$asyncQueue.length) { var asyncTask = this.$$asyncQueue.shift(); asyncTask.scope.$eval(asyncTask.expression); } dirty = this.$$digestOnce(); if ((dirty || this.$$asyncQueue.length) && !(ttl--)) { //修改的代码 throw "10 digest iterations reached"; } } while (dirty || this.$$asyncQueue.length); };
这样,当 (dirty || this.$$asyncQueue.length)反复为true时候,就会tll累减,最后抛出错误终止。
什么时候手动调用$apply 方法
取决于是否在 Angular 上下文环境(angular context)。
AngularJS对此有着非常明确的要求,就是它只负责对发生于AngularJS上下文环境中的变更会做出自动地响应(即,在$apply()方法中发生的对于models的更改)。AngularJS的 built-in 指令就是这样做的,所以任何的model 变更都会被反映到 view 中。但是,如果你在AngularJS上下文之外的任何地方修改了 model,那么你就需要通过手动调用$apply()来通知AngularJS。这就像告诉AngularJS,你修改了一些models,希望AngularJS帮你触发watchers来做出正确的响应。
典型的需要调用 $apply() 方法的场景是:
- 使用了 JavaScript 中的 setTimeout() 来更新一个 scope model
- 用指令设置一个 DOM 事件 listener 并且在该 listener 中修改了一些 models
如何优化脏检查与运行效率
脏检查效率是不高,但在非大量的检查下是可以接受的。所以在绑定大量表达式时请注意所绑定的表达式效率。建议注意一下几点:
- 表达式(以及表达式所调用的函数)中少写太过复杂的逻辑
- 不要连接太长的 filter(往往 filter 里都会遍历并且生成新数组)
- 不要访问 DOM 元素。
小结:
- 脏检查是一种模型到视图的数据映射机制,由 $apply 或 $digest 触发。
- 脏检查的范围是整个页面,不受区域或组件划分影响
- 使用尽量简单的绑定表达式提升脏检查执行速度
- 尽量减少页面上绑定表达式的个数(单次绑定和ng-if)
另外,使用单次绑定减少绑定表达式数量、善用 ng-if 减少绑定表达式的数量、给 ng-repeat 手工添加 track by等也值得注意,此文不再细述。
以上是我学习脏检查的粗略总结,错漏在所难免。谢谢你的阅读ღ( ´・ᴗ・` )
参考:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
JavaScript & jQuery
David Sawyer McFarland / O Reilly / 2011-10-28 / USD 39.99
You don't need programming experience to add interactive and visual effects to your web pages with JavaScript. This Missing Manual shows you how the jQuery library makes JavaScript programming fun, ea......一起来看看 《JavaScript & jQuery》 这本书的介绍吧!