原文连接: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)。浏览器
让咱们先看一下在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, ...)
和run
(z.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
。
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相关联,将它们有效地连接在一块儿。而后咱们能够获得如下调用栈:
稍后咱们将看到如何实现这一效果。
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);
复制代码
为了完成将a
和c
函数置于同一个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。
让咱们运用这些的知识点,简单地修改如下咱们的例子:
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
函数都在将在AB
zone中执行:
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
函数是在AB
zone中调用的,可是(出乎意料的是),c
函数也是在(AB
)这个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
复制代码
你能够看得出,当2
秒setTimeout
任务完成时,并无触发任何事件。onHasTask
钩子会在第一个setTimeout
被调度时(译者注:调度不意味着setTimeout中的回调函数被执行完成了,只是setTimeout函数被调用了
)触发,而后任务队列的状态会从非空
改变到空
,当最后一个setTimeout
的回调函数完成时,onHasTask
钩子将被触发第二次。
若是你想要跟踪特定的任务,你须要使用onSheduleTask
和onInvoke
钩子。
Zone规范中定义了两个能够跟踪特定任务的钩子:
setTimeout
之类的异步操做时,(onScheduleTask)会被执行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”
复制代码
能够经过调用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的更多信息,这里是一些很好的资源: