第一章讲述了 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 在此处已经变成了 ZoneAexpect(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 为 rootZoneexpect(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 为 zoneAexpect(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'});// 代表受开发者控制的 Zonelet 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 或者像此例一样在一段时间后 resolvesetTimeout(() => {expect(Zone.current).toBe(libZone);// Promise 是在 libZone 中 resolve 的,但是不影响监听resolve('OK');}, 500);});});appZone.run(() => {promise.then(() => {// 由于开发者控制 then 方法的执行位置// 因此回调会在相同 zone 执行// 在这个例子中,即为 appZoneexpect(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 methodZone.current.wrap(callback),delay);}// 返回一个包装过后的回调版本来恢复 zone。Zone.prototype.wrap = function(callback) {// 记录当前 zonelet 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) {// 记录当前 zonelet 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,所以本例中的包装只是为了展示概念。
