我逆向工程zone.js后的发现

原文连接:blog.angularindepth.com/i-reverse-e…javascript

做者:Max Koretskyi aka Wizardjava

翻者:而井node

Zones是一种能够帮助开发者在多个异步操做之间进行逻辑链接的新机制。经过一个zone,将相关联的每个异步操做关联起来是Zones运行的方式。开发者能够从中获益:git

  • 将数据与zone相关联,(使得)在zone中的任何异步操做均可以访问到(这些数据),在其余语言中称之为线程本地存储(thread-local storage)github

  • 自动跟踪给定zone内的未完成的异步操做,以执行清理或呈现或测试断言步骤npm

  • 统计zone中花费的总时间,用于分析或现场分析编程

  • 能够在一个zone中处理全部没有捕获的异常、没有处理和promise rejections,而非将其(异常)传导到顶层segmentfault

网上大部分(关于zone.js)的文章要么是在讲(zone.js)过期的API,要么就是用一个很是简单的例子来解释如何使用Zones。在本文中,我将使用最新的API并在尽量接近实现的状况下详细探索基本API。我将从API开始讲起,而后阐述异步任务关联机制,继而探讨拦截钩子,开发者能够利用这些拦截钩子来执行异步任务。在文末,我将简明扼要地阐述Zones底层是如何运做的。promise

Zones如今是(属于)EcmaScript标准里的stage 0状态的提案,目前被Node所阻止。Zones一般被指向为Zone.js,(Zone.js)是一个GitHub仓库和npm包的名字。然而在本文中,我将使用Zone这个名词(而非Zone.js),由于规范中依据指定了(Zone)。浏览器

相关的Zone API

让咱们先看一下在Zones中最经常使用的方法。这个Class的定义以下:

class Zone {
  constructor(parent: Zone, zoneSpec: ZoneSpec);
  static get current();
  get name();
  get parent();

  fork(zoneSpec: ZoneSpec);
  run(callback, applyThis, applyArgs, source);
  runGuarded(callback, applyThis, applyArgs, source);
  wrap(callback, source);

}
复制代码

Zones有一个关键的概念就是当前区current zone)。当前区是能够在全部异步操做之间传递的异步上下文。它表示与当前正在执行的堆栈帧/异步任务相关联的区。当前区能够经过Zone.current这个静态getter访问到。

每一个zone都有(属性)name,(这个属性)主要是为了工具链和调试中使用。同时zone中也定义了一些用来操做zones的方法:

  • z.run(callback, ...)在给定的zone中以同步的方式调用一个函数。它在执行回调时将当前区域设置为z,并在回调完成执行后将其重置为先前的值。在zone中执行回调一般被称为“进入”zone。

  • z.runGuarded(callback, ...)runz.run(callback, ...))同样,可是会捕获运行时的异常,而且提供一种拦截的机制。若是存在一个异常没有被父区(parent Zone)处理,这个异常就会被从新抛出。

  • z.wrap(callback) 会产生一个包含z的闭包函数,在执行时表现得z.runGuarded(callback)基本一致。即便这个回调函数被传入other.run(callback)(译者注:回调函数指的是z.wrap(callback)的返回值),这个回调函数依旧会在z区中执行,而非other区。这是一种相似于Javascript中Function.prototype.bind的机制。

在下一章节咱们将详细地谈论到fork方法。Zone拥有一系列去运行、调度、取消一个任务的方法:

class Zone {
  runTask(...);
  scheduleTask(...);
  scheduleMicroTask(...);
  scheduleMacroTask(...);
  scheduleEventTask(...);
  cancelTask(...);
复制代码

这里有一些开发者比较少用到的底层方法,因此我并不打算在本文中详细地讨论它们。调度一个任务是Zone中的内部操做,对于开发者而言,其意义大体等同于调用一些异步操做,例如:setTimeout

在调用堆栈中保留Zone

JavaScript虚拟机会在每一个函数它们本身的栈帧中执行函数。因此若是你有以下代码:

function c() {
    // capturing stack trace
    try {
        new Function('throw new Error()')();
    } catch (e) {
        console.log(e.stack);
    }
}

function b() { c() }
function a() { b() }

a();
复制代码

c函数中,它有如下的调用栈:

at c (index.js:3)
at b (index.js:10)
at a (index.js:14)
at index.js:17
复制代码

MDN网站上,有我在c函数中捕获执行栈的方法的描述。

调用栈以下图所示:

能够看出,除了3个栈帧是咱们调用函数时产生的,另外还有一个栈是全局上下文的。

在常规JavaScript环境中,c函数的栈帧是没法与a函数的栈帧相关联的。可是经过一个特定的zone,Zone容许咱们作到这一点(将c函数的栈帧是与a函数的栈帧相关联)。例如,咱们能够将堆栈帧a和c与相同的zone相关联,将它们有效地连接在一块儿。而后咱们能够获得如下调用栈:

稍后咱们将看到如何实现这一效果。

用zone.fork建立一个子zone

Zones中一个最经常使用的功能就是经过fork方法来建立一个新的zone。Forking一个zone会建立一个新的子zone,而且设置其父zone为调用fork方法的zone:

const c = z.fork({name: 'c'});
console.log(c.parent === z); // true
复制代码

fork方法内部其实只是简单的经过一个类建立了一个新的zone:

new Zone(targetZone, zoneSpec);
复制代码

为了完成将ac函数置于同一个zone中相关联的目的,咱们首先须要建立那个zone。为了建立那个zone,咱们须要使用我上文所展现的fork方法:

const zoneAC = Zone.current.fork({name: 'AC'});
复制代码

咱们传入fork方法中的对象被称为区域规范(ZoneSpec),其拥有如下属性:

interface ZoneSpec {
    name: string;
    properties?: { [key: string]: any };
    onFork?: ( ... );
    onIntercept?: ( ... );
    onInvoke?: ( ... );
    onHandleError?: ( ... );
    onScheduleTask?: ( ... );
    onInvokeTask?: ( ... );
    onCancelTask?: ( ... );
    onHasTask?: ( ... );
复制代码

name定义了一个zone的名称,properties则是在这个zone中相关联的数据。其他的属性是拦截钩子,这些钩子容许父zone拦截其子zone的某些操做。重要的是理解forking建立zone层次结构,以及在父zone中使用Zone类上的全部方法来拦截操做。稍后咱们将在文章中看看如何在异步操做之间使用properties来分享数据,以及如何利用钩子来实现任务跟踪。

让咱们再建立一个子zone:

const zoneB = Zone.current.fork({name: 'B'});
复制代码

如今咱们拥有了两个zone,咱们能够在特定的zone中使用它们来执行一些函数。为了达到这个目的,咱们须要使用zone.run()方法。

用zone.run来切换zone

为了在一个zone中建立一个特定的相关联的栈帧,咱们须要使用run方法。正如你所知,它以同步的方式在指定的zone中运行一个回调函数,完成以后将会恢复到以前的zone。

让咱们运用这些的知识点,简单地修改如下咱们的例子:

function c() {
    console.log(Zone.current.name);  // AC
}
function b() {
    console.log(Zone.current.name);  // B
    zoneAC.run(c);
}
function a() {
    console.log(Zone.current.name);  // AC
    zoneB.run(b);
}
zoneAC.run(a);
复制代码

如今每个调用栈都有了一个相关联的zone:

真如你所见,经过上面咱们执行的代码,使用run方法咱们能够直接指名(函数)运行于哪一个zone之中。你如今可能会想如何咱们不使用run方法,而是简单地在zone中执行函数,那会发生什么?

** 这里有一个关键点就是要明白,在这个函数中,函数内全部函数调用和异步任务调度,都将在与相同的zone中执行。 **

咱们知道在zones环境中一般都会有一个根区(root zone)。因此若是咱们不经过zone.run来切换zone,那么全部的函数将会在root zone中执行。让咱们瞧一瞧这个结果:

function c() {
    console.log(Zone.current.name);  // <root>
}
function b() {
    console.log(Zone.current.name);  // <root>
    c();
}
function a() {
    console.log(Zone.current.name);  // <root>
    b();
}
a();
复制代码

结果就是如上所述,用图表表示就是如图:

而且若是咱们只在a函数中运行zoneAB.run,那么b和c函数都在将在ABzone中执行:

const zoneAB = Zone.current.fork({name: 'AB'});

function c() {
    console.log(Zone.current.name);  // AB
}

function b() {
    console.log(Zone.current.name);  // AB
    c();
}

function a() {
    console.log(Zone.current.name);  // <root>
    zoneAB.run(b);
}

a();
复制代码

如你所见,咱们能够预期b函数是在ABzone中调用的,可是(出乎意料的是),c函数也是在(AB)这个zone中执行的。

在异步任务之间维持zone

JavaScript开发有一个鲜明的特征,那就是异步编程。可能大多数JS新手均可以熟练使用setTimeout方法来作异步编程,该方法容许推迟执行函数。Zone调用setTimeout异步操做任务。具体来讲,(setTimeout产生的)是一个宏任务。另外一类任务则是微任务,例如,promise.then。这些术语在浏览器内部所使用,Jake Archibald对任务、微任务、队列、调度作过深度的介绍说明

让咱们看看Zone中是如何处理像setTimeout这类的异步任务的。为此,咱们将使用上面使用的代码,但不是当即调用函数c,而是将它做为回调传递给setTimeout函数。因此这个回调函数将在将来的某个时间(大约2秒内),在单独的调用堆栈中执行:

const zoneBC = Zone.current.fork({name: 'BC'});

function c() {
    console.log(Zone.current.name);  // BC
}

function b() {
    console.log(Zone.current.name);  // BC
    setTimeout(c, 2000);
}

function a() {
    console.log(Zone.current.name);  // <root>
    zoneBC.run(b);
}

a();
复制代码

咱们已经了解了,若是咱们在一个zone中调用一个函数,此函数将会在同一个zone中执行。而且对于一个异步任务来讲,表现也是同样的。若是咱们调度一个异步任务并指定回调函数,那么这个回调函数将在调度任务的同一zone中执行。

因此若是咱们绘制函数调用的历史,咱们将获得下图:

看起来很是好对吧。然而,这张图隐藏了重要的实现细节。在底层,Zone必须为要执行过的每一个任务恢复正确的zone。为此,必须记住执行此任务的zone,并经过在任务上保留对关联zone的引用来实现(这一目标)。这个zone以后会在root zone的处理程序中用于调用任务。

这意味着每个异步任务的调用栈基本上都开始于root zone,root zone将使用与任务相关的信息来恢复正确的zone和调用任务。因此这里有一个更准确的表示:

在异步任务之间传递上下文

Zone有一系列开发者能够受益的有趣功能。其中之一就是上下文传递。这意味着咱们能够在zone中访问到数据,而且zone中运行的任何任务也能够访问到这些数据。

让咱们使用前一个例子,来演示咱们是如何在setTimeout异步任务中传递数据的。你已经了解到了,当forking一个新zone时,咱们能够传入一个zone规范对象。这个对象有一个可选属性properties。咱们可使用这个属性来将数据与zone作关联,以下:

const zoneBC = Zone.current.fork({
    name: 'BC',
    properties: {
        data: 'initial'
    }
});
复制代码

以后,(数据)能够经过zone.get方法来访问获得:

function a() {
    console.log(Zone.current.get('data')); // 'initial'
}

function b() {
    console.log(Zone.current.get('data')); // 'initial'
    setTimeout(a, 2000);
}

zoneBC.run(b);
复制代码

这个(数据)对象的properties是一个浅不变对象,这意味着你不能够对其(数据对象的properties属性对象)属性新增属性、删除属性的操做。这也是Zone不提供方法去作上述操做的最大缘由。因此在上面的例子中,咱们不能对properties.data设置不一样的值。

然而,若是咱们将不是原始类型、而是对象类型的值传递给properties.data,那么咱们就能够修改数据了:

const zoneBC = Zone.current.fork({
    name: 'BC',
    properties: {
        data: {
            value: 'initial'
        }
    }
});

function a() {
    console.log(Zone.current.get('data').value); // 'updated'
}

function b() {
    console.log(Zone.current.get('data').value); // 'initial'
    Zone.current.get('data').value = 'updated';
    setTimeout(a, 2000);
}

zoneBC.run(b);
复制代码

有趣的是,使用fork方法建立的子zone,会从父zone继承属性:

const parent = Zone.current.fork({
    name: 'parent',
    properties: { data: 'data from parent' }
});

const child = parent.fork({name: 'child'});

child.run(() => {
    console.log(Zone.current.name); // 'child'
    console.log(Zone.current.get('data')); // 'data from parent'
});
复制代码

跟踪未完成的任务

Zone另一个可能更加有趣和实用的功能就是,跟踪未完成的异步的宏任务、微任务。Zone将全部未完成的任务保留在一个队列之中。要想在此队列状态更改时收到通知,咱们可使用区规范(zone spec)的onHasTask钩子。这是它的类型定义:

onHasTask(delegate, currentZone, targetZone, hasTaskState);
复制代码

因为父zone能够拦截子zone事件,所以Zone提供currentZone和targetZone两个参数,用以区分任务队列中发生更改的zone和拦截事件的zone。举个例子,若是你须要确保只想拦截当前zone的事件,只须要比较一下zone(是否相同):

// We are only interested in event which originate from our zone
if (currentZone === targetZone) { ... }
复制代码

传入钩子函数的最后一个参数是hasTaskState,它描述了任务队列的状态。这里使它的类型定义:

type HasTaskState = {
    microTask: boolean; 
    macroTask: boolean; 
    eventTask: boolean; 
    change: 'microTask'|'macroTask'|'eventTask';
};
复制代码

因此若是你在一个zone中调用setTimeout,那么你将得到的hasTaskState对象以下:

{
    microTask: false; 
    macroTask: true; 
    eventTask: false; 
    change: 'macroTask';
}
复制代码

代表队列中存在未完成的macrotask,队列中的更改来自macrotask

若是咱们这么作:

const z = Zone.current.fork({
    name: 'z',
    onHasTask(delegate, current, target, hasTaskState) {
        console.log(hasTaskState.change);          // "macroTask"
        console.log(hasTaskState.macroTask);       // true
        console.log(JSON.stringify(hasTaskState));
    }
});

function a() {}

function b() {
    // synchronously triggers `onHasTask` event with
    // change === "macroTask" since `setTimeout` is a macrotask
    setTimeout(a, 2000);
}

z.run(b);
复制代码

那么,咱们会获得以下输出:

macroTask
true
{
    "microTask": false,
    "macroTask": true,
    "eventTask": false,
    "change": "macroTask"
}
复制代码

每当setTimeout完成时,onHasTask都会被再次触发:

须要注意的是,咱们只能使用onHasTask来跟踪整个任务队列空/非空状态。你不能够利用它(onHasTask)来跟踪队列中指定的任务。若是你运行以下代码:

let timer;

const z = Zone.current.fork({
    name: 'z',
    onHasTask(delegate, current, target, hasTaskState) {
        console.log(Date.now() - timer);
        console.log(hasTaskState.change);
        console.log(hasTaskState.macroTask);
    }
});

function a1() {}
function a2() {}

function b() {
    timer = Date.now();
    setTimeout(a1, 2000);
    setTimeout(a2, 4000);
}

z.run(b);
复制代码

你会获得如下输出:

1
macroTask
true

4006
macroTask
false
复制代码

你能够看得出,当2setTimeout任务完成时,并无触发任何事件。onHasTask钩子会在第一个setTimeout被调度时(译者注:调度不意味着setTimeout中的回调函数被执行完成了,只是setTimeout函数被调用了)触发,而后任务队列的状态会从非空改变到,当最后一个setTimeout的回调函数完成时,onHasTask钩子将被触发第二次。

若是你想要跟踪特定的任务,你须要使用onSheduleTaskonInvoke钩子。

onSheduleTask 和 onInvokeTask

Zone规范中定义了两个能够跟踪特定任务的钩子:

  • onScheduleTask 检查到相似setTimeout之类的异步操做时,(onScheduleTask)会被执行
  • onInvokeTask 传入异步操做、如setTimeout之中的回调函数被执行时,(onInvokeTask)会被执行

如下就是如何使用这些钩子来跟踪各个任务(的例子):

const z = Zone.current.fork({
    name: 'z',
    onScheduleTask(delegate, currentZone, targetZone, task) {
      const result = delegate.scheduleTask(targetZone, task);
      const name = task.callback.name;
      console.log(
          Date.now() - timer, 
         `task with callback '${name}' is added to the task queue`
      );
      return result;
    },
    onInvokeTask(delegate, currentZone, targetZone, task, ...args) {
      const result = delegate.invokeTask(targetZone, task, ...args);
      const name = task.callback.name;
      console.log(
        Date.now() - timer, 
       `task with callback '${name}' is removed from the task queue`
     );
     return result;
    }
});

function a1() {}
function a2() {}

function b() {
    timer = Date.now();
    setTimeout(a1, 2000);
    setTimeout(a2, 4000);
}

z.run(b);
复制代码

预期输出:

1 “task with callback ‘a1’ is added to the task queue”
2 “task with callback ‘a2’ is added to the task queue”
2001 “task with callback ‘a1’ is removed from the task queue”
4003 “task with callback ‘a2’ is removed from the task queue”
复制代码

使用onInvoke拦截zone的进入

能够经过调用z.run()显式地进入(切换)zone,也能够经过调用任务来隐式进入(切换)zone。在上一节中,我解释了onInvokeTask挂子,当Zone内部执行与异步任务相关联的回调时,该钩子可用于拦截zone的进入。还有另外一个钩子onInvoke,您能够经过运行z.run()在进入zone时收到通知。

如下是如何使用它的示例:

const z = Zone.current.fork({
    name: 'z',
    onInvoke(delegate, current, target, callback, ...args) {
        console.log(`entering zone '${target.name}'`);
        return delegate.invoke(target, callback, ...args);
    }
});

function b() {}

z.run(b);
复制代码

将输出:

entering zone ‘z’
复制代码

`Zone.current`底层是如何运行的

当前zone被这里的闭包中使用_currentZoneFrame变量所跟踪着,它(_currentZoneFrame)被Zone.current这个getter所返回。因此为了切换zone,须要简单地更新如下_currentZoneFrame的值。如今,你能够经过z.run()或调用任务来切换zone。

这里run方法更新变量的地方:

class Zone {
   ...
   run(callback, applyThis, applyArgs,source) {
      ...
      _currentZoneFrame = {parent: _currentZoneFrame, zone: this};
复制代码

runTask方法更新变量的地方在这里

class Zone {
   ...
   runTask(task, applyThis, applyArgs) {
      ...
      _currentZoneFrame = { parent: _currentZoneFrame, zone: this };
复制代码

在每一个任务中invokeTask方法会调用runTask方法

class ZoneTask {
    invokeTask() {
         _numberOfNestedTaskFrames++;
      try {
          self.runCount++;
          return self.zone.runTask(self, this, arguments);
复制代码

建立的每一个任务时都会在zone属性中保存其zone。这正是用于在invokeTask中运行任务的zone(self指的是此处的任务实例):

self.zone.runTask(self, this, arguments);
复制代码

其余资源

若是您想得到有关Zone的更多信息,这里是一些很好的资源:

相关文章
相关标签/搜索