kittencup / angular2-ama-cn

angular2 随便问

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

理解Zone

kittencup opened this issue · comments

原文地址: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,当然还有更多的例子

zone.fork()是fork出了一个新的zone,那么如果有

foo = methodLog("foo"),
bar = methodLog("bar"),
baz = function(){
    setTimeout(methodLog('baz in setTimeout'), 0);
};

三个函数在run()中执行,这三个函数是在同一个zone中吗,还是那个异步函数baz在一个独立的zone里?