第一章讲述了 Zone 如何创建并且在异步任务中传播。
第一节:进入 Zone, Forking 和栈帧
首先要理解 zone 是如何创建(fork),在系统间传播,以及如何在心中构建 zone 和函数的模型。
// rootZone 是所有代码的执行环境,和未分配 zone 没有区别
let rootZone = Zone.current;
// 通过 fork 一个已经存在的 zone 来创建新的 zone.
let zoneA = rootZone.fork({name: 'zoneA'});
// 每个 zone 都有一个 name 属性用于调试
expect(rootZone.name).toEqual('<root>');
expect(zoneA.name).toEqual('zoneA');
// 子 zone 能访问其父 zone(单向引用)
expect(zoneA.parent).toBe(rootZone);
function main() {
// zone 只能通过 `run 或 runGuarded 或 runTask` 方法执行.
zoneA.run(function fnOuter() {
// Zone.current 在此处已经变成了 ZoneA
expect(Zone.current).toBe(zoneA);
// 每个栈帧都和一个 zone 相关联
expect(Error.captureStackTrace()).toEqual(outerLocation)
// zone 的嵌套和栈帧的嵌套是相同的
rootZone.run(function fnInner() {
// 某个方法与其被调用时的方法处于同一个 Zone 内
// There is no reason why a nested stack frame must be
// a child of parent stack frame zone.
// This is how one can "escape" a zone.
expect(Zone.current).toBe(rootZone);
expect(Error.captureStackTrace()).toEqual(innerLocation)
});
});
}
main();
重点:
- 直接修改 Zone.current 会产生一个运行时错误。 唯一能改变 Zone.current 的方法是调用一个已经存在的 zone 的
Zone.prototype.run()
或Zone.prototype.runGuarded()
或Zone.prototype.runTask()
方法,在后续文档中,为了方便,我们会使用Zone.prototype.run()
方法来代指这三种方法。 - 一个给定的栈帧会有一个确定的 zone 与之关联。 一个栈帧的上方或者下方一定处于相同的zone,除非这个栈帧是
Zone.prototype.run().
- 子 zone 有一个其父 zone 的引用 (但是父 zone 没有子 zone 的引用)获取父级 zone 仅允许通过简单的释放引用来进行垃圾回收。
栈帧
一个给定的栈帧只能完整的与一个zone相关联,理解这一点非常重要。(例如,一个函数不可能前半部分关联一个 zone ,后半部分关联另一个 zone ,但是有可能在一个调用中完整的关联一个 zone,在另一个调用中完整地关联另一个 zone)。Zone 只能通过Zone.prototype.run()
进入和离开。zone 通过更新调用栈来更好地显示 zone。下面是上方代码的调用栈的输出。
outerLocation:
at fnOuter()[zoneA];
at Zone.prototype.run()[<root> -> zoneA]
at main()[<root>]
at <anonymous>()[<root>]
innerLocation:
at fnInner()[<root>];
at Zone.prototype.run()[zoneA -> <root>]
at fnOuter()[zoneA];
at Zone.prototype.run()[<root> -> zoneA]
at main()[<root>]
at <anonymous>()[<root>]
第二节:追踪异步操作
我们已经看过如何进入和离开 zone,现在看看 zone 在异步任务中是如何维持的。
let rootZone = Zone.current;
let zoneA = rootZone.fork({name: 'A'});
expect(Zone.current).toBe(rootZone);
// 此处调用 setTimeout 的 zone 为 rootZone
expect(Error.captureStackTrace()).toEqual(rootLocation)
setTimeout(function timeoutCb1() {
// 回调函数也是在 rootZone 中执行
expect(Zone.current).toEqual(rootZone);
expect(Error.captureStackTrace()).toEqual(rootLocationRestored)
}, 0);
zoneA.run(function run1() {
expect(Zone.current).toEqual(zoneA);
// 此处调用 setTimeout 的 zone 为 zoneA
expect(Error.captureStackTrace()).toEqual(zoneALocation)
setTimeout(function timeoutCb2() {
// 回调函数也是在 zoneA 中执行
expect(Zone.current).toEqual(zoneA);
expect(Error.captureStackTrace()).toEqual(zoneALocationRestored)
}, 0);
});
rootLocation:
at <anonymous>()[rootZone]
rootLocationRestored:
at timeoutCb1()[rootZone]
at Zone.prototype.run()[<root> -> <root>]
zoneALocation:
at run1()[zoneA]
at Zone.prototype.run()[<root> -> zoneA]
at <anonymous>()[rootZone]
zoneALocationRestored:
at timeoutCb2()[zoneA]
at Zone.prototype.run()[<root> -> zoneA]
重点:
当异步任务执行的时候,回调函数会在调用异步任务时所处的 zone 中执行,这允许 zone 在异步任务调用时追踪任务。
使用 Promise 时类似的例子(Promise 有些许不同,因为它使用自身的回调处理错误)
let rootZone = Zone.current;
// LibZone 代表不受开发者的一些第三方库
// 本次例子不想大多数第三方库有细粒度的 zone 控制
let libZone = rootZone.fork({name: 'libZone'});
// 代表受开发者控制的 Zone
let appZone = rootZone.fork({name: 'appZone'});
let appZone1 = rootZone.fork({name: 'appZone1'});
// 这个例子是为了演示 promise 的执行和回调的区别
// Promise 的执行可能会出现在第三方库的 zone 中
let promise = libZone.run(() => {
return new Promise((resolve, reject) => {
expect(Zone.current).toBe(libZone);
// Promise 可以立即 resolve 或者像此例一样在一段时间后 resolve
setTimeout(() => {
expect(Zone.current).toBe(libZone);
// Promise 是在 libZone 中 resolve 的,但是不影响监听
resolve('OK');
}, 500);
});
});
appZone.run(() => {
promise.then(() => {
// 由于开发者控制 then 方法的执行位置
// 因此回调会在相同 zone 执行
// 在这个例子中,即为 appZone
expect(Zone.current).toBe(appZone);
});
});
appZone1.run(() => {
promise.then(() => {
// 不同的 then 回调可以在不同的 zone 中调用
expect(Zone.current).toBe(appZone1);
});
});
重点:
- 对于 Promise,then 方法的回调所处的 zone 与调用 then 方法时的 zone 相同。
- 你可以给 then 的回调指定不同的 zone,比如创建 promise 时所处的 zone 或者 resolve 时所处的 zone,但是不推荐这么做因为 Promise 的创建和 resolve 可能处于第三方库内,生成的 Promise 可能随后在应用内的 zone 中继续执行,开发者会希望这个操作在其应用所在的 zone 中继续传播。
例如:调用 fetch()
会返回一个 Promise,基于自身考虑 fetch()
可能会在其内部使用自己的 zone,应用在调用 .then()
的时候会希望其运行在自己内部的 zone 中。(我们当然不希望 fetch()
的 zone 泄漏到我们的应用中。)
第三节:为异步程序打补丁
现在看看 zone 在异步调用之间传播的底层机制。(注:实际的工作会更复杂一点,因为他们会通过后面将会提到的任务创建机制进行创建,为了更清晰地展示 zone 的底层机制,例子会非常简单。)
// 保存原生 setTimeout 的引用
let originalSetTimeout = window.setTimeout;
// 使用包装了 zone 回调的方法重写 API。
window.setTimeout = function(callback, delay) {
// 调用原生 API 但是包装了 zone 的回调
return originalSetTimeout(
// Wrap the callback method
Zone.current.wrap(callback),
delay
);
}
// 返回一个包装过后的回调版本来恢复 zone。
Zone.prototype.wrap = function(callback) {
// 记录当前 zone
let capturedZone = this;
// 返回一个在当前 zone 中执行源闭包的闭包。
return function() {
// 在记录下的当前 zone 中执行源回调
return capturedZone.runGuarded(callback, this, arguments);
};
};
重点:
- zone 只进行一次猴子补丁(monkey patch)
- 进入/离开 zone 只需要改变 Zone.current 的值(不需要额外的猴子补丁)
Zone.prototype.wrap
方法是为包装回调提供便利。(包装后的回调函数是在Zone.prototype.runGuarded()
中执行的),(注:此处的Zone.prototype.wrap
只是为了方便使用,当前的 zone 通过创建任务来给所有异步方法打补丁)Zone.prototype.runGuarded()
和Zone.prototype.run()
类似,只不过多了额外的try-catch
语句来捕获错误。
// 保存原生 Promise.prototype.then 的引用。
let originalPromiseThen = Promise.prototype.then;
// 使用包装了参数的函数重写 API
// 注:真实的 API 具有更多参数,这里只是一个简单的演示版本
Promise.prototype.then = function(callback) {
// 记录当前 zone
let capturedZone = Zone.current;
// 返回一个在当前 zone 中执行源闭包的闭包
function wrappedCallback() {
return capturedZone.run(callback, this, arguments);
};
// 在记录下的当前 zone 中执行源回调。
return originalPromiseThen.call(this, [wrappedCallback]);
};
重点:
- Promise 使用他们自带的错误处理流程来捕获错误,因此无法使用
Zone.prototype.wrap()
。(我们也可以使用其他 API 来解决这个问题,但是 Promise 是例外规则,因此我们觉得把他的 API 修改为自己的版本是不合理的。) - Promise API 不想上面例子中那样清晰明确,他更加庞大。
- Zone 使用基于微任务的猴子补丁实现了 ZoneAwarePromise,所以本例中的包装只是为了展示概念。