angular2 脏检查总述
这系列文章将介绍angular2的脏值检查是如何工作的?如何比ng1更高效?带着上述问题,让我们一起来看看angular2这禽兽(谁让它叫angular,又那么生猛)干了什么。
什么是脏值检查
片面的说脏检查是对比当前的数据和曾经的数据是否发生改变。而在这个context下,我想介绍的是angular2从发现数据的变化到找到变化的点到更新DOM的整个过程。也就是说这里所说的脏值检查是viewmodel与view层的那座桥梁。先看下面的图,红色表示改变的节点。
那么问题来了,angular2是如何知道数据发生了改变?又是如何知道需要修改DOM的位置,准确的最小范围的修改DOM呢?没错,尽可能小的范围修改DOM,因为操作DOM对于性能来说可是一件奢侈品。别急,让我们先看看没有angular我们如何实现数据改变到view的改变。在web古老的年代,那个asp.net、j2ee、PHP的时代,请求+整页重绘,那时啪啪啪的重绘声,如今依然回荡在心中,痛苦不可磨灭。再来看看SPA时代,其它framework的解决方案,最值得一提的是名声在外的react用了diff虚拟DOM的方式,也实现了最小化更新DOM。有兴趣可以看看Tero的这篇博客,比较了很多流行框架对这个问题的解决。
http://teropa.info/blog/2015/...
回到上面问题,angular2是如何知道数据发生了改变?细心的你可能会发现,在angular2 示例项目中都引入了一个Zone.js的东西。Zone.js是什么鬼?
angular2 脏检查Series1之Zone.js
Zone能做什么?
Zone提供方便的方式”进入”异步函数执行上下文(注意进入有引号,后面解释),并能在异步执行环境中加入一些钩子的东西。
为什么需要进入异步函数的执行上下文?这是我看到zone.js的github的第一个问题。我们先来看看这样一个场景。
foo(); setTimeout(doSomething,2000); bar(); baz();
我任性的提出一个问题,我想知道上面doSomething函数在这个上下文中什么时候开始执行的?要知道为了不阻塞UI界面的用户体验,在JavaScript执行的很多耗时操作都被封装为了异步操作,如:setTimeout、XMLHttpRequest、DOM事件等。也就是说doSomething会进入事件循环。 这个时候是不是特别期望,能进入doSomething的执行环境,拿到点证据控告写doSomething这个函数的程序员写得垃圾?可能你已经想到了解决办法,虽然doSomething的执行上下文我进不了。但我可以wrap一下doSomething伪造一个执行上下文,在这个上下文中做点手脚,哼哼.. 恭喜你,你已经有了和Zone.js团队成员一样的思想觉悟。
这也是为什么上面提到的Zone提供方便的方式”进入”异步函数执行上下文中进入加了引号。并不是真正的进入,而是通过包裹的方式伪造执行上下文,并通过钩子函数方便的进入执行环境。这个场景看似有些极端,但在异步Task跟踪,分析,错误记录、开发调试跟踪等场景,都有这样的需求。下面我们来看看Zone是如何提供方便的。
Zone如何使用
demo1
var profilingZone = (function () { var time = 0,timer = performance ? performance.now.bind(performance) : Date.now.bind(Date); return { beforeTask: function () { this.start = timer(); console.log(‘beforeTask time:’+this.start); },afterTask: function () { time += timer() - this.start; console.log(‘afterTask time:’+time); } }; }()); zone.fork(profilingZone).run(function(){ foo(); setTimeout(doSomething,2000); bar(); baz(); });
demo1运行结果
// beforeTask time:3073872.9000000004 // AfterTask time:1.04500000039116 // beforeTask time:3075873.165 // AfterTask time:1.2550000004
可以从上面的demo看到运用Zone提供的beforeTask,afterTask钩子函数方便的进入了doSomething执行的上下文,记录了时间。值得一提的是,我们并没有对doSomething做任何处理,我们所做的只是在doSomething外部做了点改动。就达到了进入doSomething执行上下文的目的。似乎看到了AOP的思想(说到AOP我又想到了ng2的annotation,找个时间好好分享一下)。 除此之外Zone还提供了一些其它钩子函数。请参考:https://github.com/angular/zo...
Zone原理
yo! check it out! demo的运行结果为什么会有输出两次beforeTask和AfterTask?要想解答这个问题,我们先来看看Zone运行的原理。前面提到过Zone伪造一个执行上下文,实际上Zone有一个叫猴子补丁的东西。在Zone.js运行时,就会为这些异步事件做一层代理包裹,也就是说Zone.js运行后,调用setTimeout、addEventListener等浏览器异步事件时,不再是调用原生的方法,而是被猴子补丁包装过后的代理方法。wo!猴子补丁真牛逼,它是怎么把这些原生的事件都进行包装改造后进化成“猴子”的呢?其实很简单,其实并不难..只需要暴力点!再暴力点!
//以下是Zone.js启动时执行逻辑的抽象代码片段 function zoneAwareAddEventListener() {...} function zoneAwareRemoveEventListener() {...} function zoneAwarePromise() {...} function patchTimeout() {...} window.prototype.addEventListener = zoneAwareAddEventListener; window.prototype.removeEventListener = zoneAwareRemoveEventListener; window.prototype.promise = zoneAwarePromise; window.prototype.setTimeout = patchTimeout;
确实很暴力,直接原生覆盖了!原生的异步方法都被代理覆盖了,代理里setup了钩子函数,这还不能完全解决问题。我们还有个需求,需要“因人而异”的处理这些暴露的钩子函数。例如
setTimeout(doA,2000); setTimeout(doB,2000);
这里有两个方法doA和doB,总不能用钩子函数里只能做同样的事情吧。所以会有一个根zone和fork。fork可以扩展一个新的zone。而每个zone都有自己的生命周期。为了理解这个问题我们再来看个Demo
demo2
//fork一个新的zone,我们给它暂定个名字叫temporary zone. Zone.current.fork({}).run(function () { //调用beforeTask等钩子(zone内部处理) //run 内部Zone.current指向temporary zone(zone内部做的处理),并添加一个inTheZone属性设置为true. Zone.current.inTheZone = true; //调用被猴子补丁包装后的setTimeout方法,并将包装后的greet方法内部的zone设置成当前的temporary zone,并将函数greet加入事件循环. setTimeout(function greet() { console.log('in the zone: ' + !!Zone.current.inTheZone); },0); //要在zone run中执行的内容已经执行完了,调用AfterTask钩子.(zone内部处理) // //调用afterTask等钩子.(zone内部处理) //zone.current引用替换成根zone,因为run外部的zone不应该是fork后的zone,fork后的zone生命周期随着run的结束而结束.(zone内部处理) }); console.log('in the zone: ' + !!Zone.current.inTheZone);
demo2输出结果
in the zone: false in the zone: true
希望更好的理解,我在demo中加了注释以说明zone生命周期的问题.我们可以看到fork后的temporary zone生命周期随着run执行的结束而结束.所以run外部的console.log取不到Zone.current里的属性inTheZone(temporary zone中的inTheZone)而在greet真正执行时,也会经历和run内部一样的过程(钩子函数的执行,zone的引用替换销毁等).而包裹后的greet内部的zone指向的是在setTimeout传入greet上下文中的(当前作用域中)temporary zone.
现在再回头看看demo1中为什么会输出两次beforeTask和AfterTask,也正是因为zone特定的生命周期所造成的.
Zone.js在angular2中的运用
还记得大明湖畔ng1的$scope.$apply吗?任何原生的事件都不会触发脏检查,必须得调用$scope.$apply来告诉angular。我的数据有更新了,你同步更新下UI吧。而在angular2中有了Zone.js。原生随便用,setTimeout,addEventListener、promise等都在ngZone中执行,angular并在ngZone中setup了相应的钩子,通知angular2做相应的脏检查处理,然后更新DOM。
如何脏检查?如何更新DOM?比起angular1有什么新的变化?下章再见。希望上述内容能给你一些帮助。如有任何疑问与不足,欢迎指出并讨论。