了解Zones

来源:互联网 发布:淘宝北京新大陆可靠吗 编辑:程序博客网 时间:2024/06/05 07:18

本文翻译自Pascal Precht的《Understanding Zones》

在NG-Conf 2014大会上,Brian进行了一场关于zones的精彩演讲,为我们很好的介绍了zones是如何改变我们处理异步代码的方式。如果你还没看过那个演讲视频,可以去看看,它只有差不多17分钟。API可能和现在有点不同了,但是语义和基本概念是一样的。在本文中,我们会去深入了解zones到底是如何工作的。

面临的问题

让我们先快速的了解下zones到底是什么。就Brian在他的演讲中所陈述的,zones基本上就是异步操作的执行上下文。实践证明,zones对于错误处理和分析非常有用。但这到底是什么意思呢?

为了理解概念中的执行上下文部分,我们需要一个zones到底是尝试解决什么问题的直观画面。让我们先一起来看看下面的这段JavaScript代码。

foo();bar();baz();function foo() {...}function bar() {...}function baz() {...}

上面的代码没有什么特别之处。我们定义了三个函数foo,bar,baz,并让他们按顺序执行。让我们来看看,当我们想要计算出执行这些代码需要多少时间的时候怎么办。其实很简单,我们只需要讲前面的代码片段像下面代码一样进行一些简单的扩张就行。

var start,    time = 0;    timer = performance ? performance.now : Date.now;// start timerstart = timer();foo();bar();baz();// stop timertime = timer() - start;// log time in msconsole.log(Math.floor(time*100) / 100 + 'ms');

但是,我们经常进行一些异步操作。例如,通过AJAX请求从远程服务器获取数据,亦或者是我们想将一些操作放在下一个时间片执行。无论这个异步操作是什么,它都会异步地执行。简而言之,这些操作不会被我们的分析器注意。让我们来看看下面的代码片段:

function doSomething() {  console.log('Async task');}// start timerstart = timer();foo();setTimeout(doSomething, 2000);bar();baz();// stop timertime = timer() - start;

我扩展了我们的代码,但这次采用的是异步方式。这对我们的分析会有什么样的影响呢?当然,我们发现其实并没有什么大的不同。

事实上,代码中只是多了一个操作,它会花费一段时间来执行,但是它的实际执行时间,也就是setTimeout()的回调函数执行时间并不在我们的分析之中。这是因为异步操作被添加进了浏览器的事件队列,当事件轮询到该操作的时候,才会被执行。

如果你对这些知识点完全不了解,我们建议你去看一个关于浏览器的事件轮询是如何工作的精彩演讲视频。

那我们如何解决这个问题呢?我们需要一些技巧,允许我们无论异步操作在什么时候发生,都可以执行我们的分析代码。当然,我们也许可以手动地为每个异步操作创建并启动一个单独的定时器,但这些定时器会被作为异步操作被添加到代码序列,这将显得相当杂乱。

这就到了zones的用武之地了。Zones可以在每次执行一个操作(好比启动或停止一个计时器,或者保存一个堆栈追踪)的时候,让代码进入或退出一个zone。Zones可以在我们的代码内重写方法,甚至用独立的zone关联数据。

创建,分叉,扩展 Zones

Zones实际上是Dart的一个语言特性。然而,当Dart可以编译成JavaScript后,我们可以在JavaScript中实现同样的功能特性。Brian已经帮我们完成了这个工作。他为JavaScript创建了zone.js作为Zones的入口,而zone.js同样是Angular 2的一个依赖库。在我们尝试了解如何通过Zones分析我们代码示例前,让我们先来讨论下,zones是怎样创建的。

一旦我们将zone.js嵌入到我们的网页中,我们就获得了一个全局的zone对象。zone有一个run()方法,它可以将一个函数作为它的参数,而该函数将在该zone中执行。换而言之,如果我们想让我们的代码在zone中运行,我们可以向下面这样做:

function main() {  foo();  setTimeout(doSomething, 2000);  bar();  baz();}zone.run(main);

Ok,非常棒!但是这是什么意思呢?现在我们除了多写了些代码外,结果其实没什么不同。然而,在这个时候,我们的代码是在一个zone中执行的(另一个执行上下文),就像我们之前了解的一样,。Zones可以在每次执行一个操作的时候,让代码进入或退出一个zone。
为了建立这些hooks,我们需要fork当前的zone.Fork一个zone会返回一个新的zone,这个新的zone是继承自父zone的。当然,fork一个zone也允许我们扩展返回的zone的行为。我们可以通过在zone对象上调用.fork()来fork一个zone。我们可以先看看下面的代码:

var myZone = zone.fork();myZone.run(main);

通过fork得到的zone和原始zone有着相同的功能。让我们来尝试通过之前提到过的这些hooks来扩展我们的新zone。我们使用ZoneSpecification来定义hooks.我们可以利用下面的这些hooks:

  • onZoneCreated - 当zone被fork时运行
  • beforeTask - 在zones中的一个函数运行后执行
  • afterTask - 在zones中的一个函数运行后执行
  • onError - 当传给zone.run的函数抛出异常时执行

下面的代码扩展了zone,在每个任务执行的前后打印日志。

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

Oh 等等,这到底是个什么鬼?每个hooks都执行了两次?这是为什么?当然,我们已经了解到zone.run明显的也被当做了一个任务,这就是为什么前两个信息被打印。但是setTimeout()好像也被当成了一个任务。这怎么可能?

Monkey-patched Hooks

事实证明,还有一些其他的hook。实际上,他们又不仅仅是hook,而monkey-patched方法在全局作用域。只要我们在我们的网页中嵌入zone.js,几乎所有引发异步操作的方法都是nokey-patched,并在新的zone中运行。

就好像当我们调用setTimeout(),我们实际上调用了Zone.setTimeout(),这反过来通过zone.fork()创建了一个新的zone,而给定的处理程序就在这新的zone中执行。这就是为什么我们的hooks也被执行了,因为fork的zone中的处理程序会执行,而fork的zone简单的继承自父zone。
还有很多方法被zone.js重写,并作为hook提供给我们:

  • Zone.setInterval()
  • Zone.alert()
  • Zone.prompt()
  • Zone.requestAnimationFrame()
  • Zone.addEventListener()
  • Zone.removeEventListener()

    我们可能想知道为什么像alert()和prompt()这样的方法也会被patched。就像前面提到的,这些pathched方法同时也是hook。我们能够通过fork一个zone改变和扩展他们,就像前面提到的befeTask和afterTask一样。这是非常强大的功能,因为当我们写测试的时候,我们可以截断alert()和prompt()的调用,并且改变他们的行为。

zone.js附带一个小型的DSL,它允许你增加zone hook,如果你对这些特殊的东西感兴趣,你可以去看看该项目的readme。

创建一个 Profiling Zone

我们最初的问题就是我们不能获取我们代码内异步任务的执行时间。现在使用Zones和它提供的API,我们完全可以创建一个zone来分析我们异步任务的CPU时间。幸运的是,在zone.js仓库中,我们已经有了一个完整的profiling zone实现。

代码如下:

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 speicification。这个例子增加了一个.time()和.reset()方法到zone,我们可以通过下面的方式在zone对象上调用:

zone  .fork(profilingZone)  .fork({    '+afterTask': function () {      console.log('Took: ' + zone.time());    }  })  .run(main);

+语法是DSL的速记,它允许我们扩展父zone的hook。很优雅哈?

0 0
原创粉丝点击