原文地址:http://blog.thoughtram.io/angular/2016/01/22/understanding-zones.html
在NG-Conf 2014年,Brian介绍了Zone,以及它们如何改变我们处理异步代码的方式。如果你还没看过这个演讲,试一试,只需要15分钟,api现在可能有所不同,但语义和底层概念都是相同的。在本文中,我们想深入地探讨zone如何的运作。
要解决的问题
让我们快速来概括下什么是Zone,正如Brian所说,它们基本上是一个异步操作的执行上下文,证明了它们在错误处理和分析非常有用,但这到底意味着什么呢?为了理解执行上下文这一部分,我们需要更好地了解Zone用来解决什么问题,我们先看看以下JavaScript代码。
foo();
bar();
baz();
function foo() {...}
function bar() {...}
function baz() {...}
在这里没什么特别的代码,我们有三个连续执行的函数foo
,bar
,baz
,比如,我们要测量这个代码的执行时间,我们很容易的扩展一些用来分析的代码段。
var start,
time = 0;
timer = performance ? performance.now || Date.now;
// start timer
start = timer();
foo();
bar();
baz();
// stop timer
time = timer() - start;
// log time in ms
console.log(Math.floor(time*100) / 100 + 'ms');
然而,我们常常有异步操作要做。可以是AJAX请求从远程服务器获取一些数据,或者也许我们只是为下一帧执行一些操作,无论发生哪种异步操作,因为是异步,基本上,这些操作无法被我们的分析代码给计算到,看看这个代码段
function doSomething() {
console.log('Async task');
}
// start timer
start = timer();
foo();
setTimeout(doSomething, 2000);
bar();
baz();
// stop timer
time = timer() - start;
我们在代码中添加了一个异步操作,这对我们的分析有什么影响?我们会发现分析结果没有什么大的差别。
事实上多了一个操作,所以需要更长的时间执行这段代码,然而实际执行的时间没有计算到setTimeout()
操作,这是因为异步操作会被添加到浏览器的事件队列,在下一次事件循环(event loops)中才会被执行。
如果你还不了解这块内容,你可以看看这个视频浏览器事件循环是如何工作的。
那么,我们如何解决这个问题,我们需要的一些hook,允许我们在这样的异步任务发生时执行一些分析代码,当然,我们可以手动的为每个异步创建并启动一个计时器,但这会使我们的代码变得非常混乱。
这就是Zone可以发挥作用的地方,Zone可以执行一些操作 - 如在每次代码进入或退出一个区域,启动、停止计时器,或保存堆栈跟踪,他们可以在我们的代码中重写方法,甚至关联起各个区域的数据。
创建(Creating),分叉(forking),扩展(extending) Zone
Zone实际上Dart语言的特性,然后,由于Dart也只是编译成JavaScript,所以我们在Javascript中也能实现相同功能,Brian做到了这点,他为Javascript Zone 创建了 Zone.js,也是一个Angular 2的依赖。在使用Zone为我们的示例代码创建分析代码之前,先让我们讨论如何创建zone。
一旦我们嵌入zone.js到我们的网站,我们可以获得全局zone对象。zone配备了一个run()
方法,它接受一个函数用来在这个zone区域中执行,也就是说,我们想要在一个zone中运行代码,我们可以这样做:
function main() {
foo();
setTimeout(doSomething, 2000);
bar();
baz();
}
zone.run(main);
酷。但这有什么意义?好吧……目前的结果没有什么区别,除了我们不得不写下更多的代码。但是,在此时,我们的代码运行在一个zone中(另一个执行上下文),正如我们前面了解到的,当我们的代码进入或退出某个zone时,zone可以对其进行操作。
为了建立这些hook,我们需要fork当前的zone,fork一个zone会返回一个新的zone,它基本上是从“父”zone继承的,当然,fork一个zone也允许我们扩展返回的那个zone的行为,我们可以在zone对象上使用.fork()
来fork一个zone,这里的代码看上去可能是这样的:
var myZone = zone.fork();
myZone.run(main);
这实际上只是给了我们一个新的zone,和原先的zone(我们还没有讨论过)相同功能。让我们来尝试这些我们之前提到的hook,并扩展我们的新zone,使用一个ZoneSpecification
来定义hook,并传递给fork()
,我们可以使用下面这些hook:
- onZoneCreated - zone被fork时调用
- beforeTask - 在zone.run执行的函数之前调用
- afterTask - 在zone.run执行的函数之后调用
- onError - 当函数传递给
run
或beforeTask抛出异常时被调用。
下面是我们的示例代码,在每个任务执行之前和之后:
var myZoneSpec = {
beforeTask: function () {
console.log('Before task');
},
afterTask: function () {
console.log('After task');
}
};
var myZone = zone.fork(myZoneSpec);
myZone.run(main);
// Logs:
// Before task
// After task
// Before task
// Async task
// After task
等一下!发生了什么?这两个hook被执行了两次? 这是为什么?当然,我们已经了解到,zone.run
显然被认为是一个“task”,这也就是为什么前两个消息被log,但似乎像setTimeout()
调用也被视为一个task了。这怎么可能?
猴子补丁(Monkey-patched) hook
Monkey-patched是指给内置对象扩展的一种术语
原来还有一些其他的hook,实际上,这些都不只是简单的hook,还在全局作用域中monkey-patched一些方法,只要我们在网站上嵌入zone.js,几乎导致所有的异步操作方法被monkey-patched,并都运行在一个新的zone里。
例如,当我们调用setTimeout()
,实际上我们调用的是Zone.setTimeout()
, 这又使用zone.fork()
创建了一个新的zone,其给定的处理程序被执行。这就是为什么我们的hook被很好的执行了,因为这个被fork的zone从父zone继承了要执行的task。
默认情况下zone.js重写了提供了如下的方法:
- Zone.setInterval()
- Zone.alert()
- Zone.prompt()
- Zone.requestAnimationFrame()
- Zone.addEventListener()
- Zone.removeEventListener()
可能有人会问,为什么像方法alert()
和prompt()
也被修补,如前所述,这些hook同时修补方法,我们可以已添加afterTask和afterTask完全相同的方式,改变和扩展它们fork的zone,这是非常强大的,当我们编写测试时,我们可以截获alert()
和prompt()
,并改变它们自己的行为。
zone.js配备了一个微型的DSL,让你可以加强zone hook,如果你对这个特别的东西感兴趣,你可以看看这个项目的readme。
创建Zone性能分析
我们最初的问题是,我们能不能捕捉到我们代码中异步任务的执行时间,现在我们已经了解关于Zone和它提供的api,实际上我们需要创建一个zone,用来记录我们异步任务的CPU时间,幸运的是,一个zone性能分析的实现在zone.js资源库例子中已经实现,你可以在这里找到
在这看上去是这样的:
var profilingZone = (function () {
var time = 0,
timer = performance ?
performance.now.bind(performance) :
Date.now.bind(Date);
return {
beforeTask: function () {
this.start = timer();
},
afterTask: function () {
time += timer() - this.start;
},
time: function () {
return Math.floor(time*100) / 100 + 'ms';
},
reset: function () {
time = 0;
}
};
}());
和我们在本文的开头的代码几乎相同,只是把他放在zone specification内,这个例子还增加了add()
和reset()
方法,调用zone对象看上去是这样的:
zone
.fork(profilingZone)
.fork({
'+afterTask': function () {
console.log('Took: ' + zone.time());
}
})
.run(main);
+语法是一个DSL,它允许扩展父zone的hook
我们还可以使用一个LongStackTraceZone,当然还有更多的例子