第一章讲述了 Zone 如何创建并且在异步任务中传播。

第一节:进入 Zone, Forking 和栈帧

首先要理解 zone 是如何创建(fork),在系统间传播,以及如何在心中构建 zone 和函数的模型。

  1. // rootZone 是所有代码的执行环境,和未分配 zone 没有区别
  2. let rootZone = Zone.current;
  3. // 通过 fork 一个已经存在的 zone 来创建新的 zone.
  4. let zoneA = rootZone.fork({name: 'zoneA'});
  5. // 每个 zone 都有一个 name 属性用于调试
  6. expect(rootZone.name).toEqual('<root>');
  7. expect(zoneA.name).toEqual('zoneA');
  8. // 子 zone 能访问其父 zone(单向引用)
  9. expect(zoneA.parent).toBe(rootZone);
  10. function main() {
  11. // zone 只能通过 `run 或 runGuarded 或 runTask` 方法执行.
  12. zoneA.run(function fnOuter() {
  13. // Zone.current 在此处已经变成了 ZoneA
  14. expect(Zone.current).toBe(zoneA);
  15. // 每个栈帧都和一个 zone 相关联
  16. expect(Error.captureStackTrace()).toEqual(outerLocation)
  17. // zone 的嵌套和栈帧的嵌套是相同的
  18. rootZone.run(function fnInner() {
  19. // 某个方法与其被调用时的方法处于同一个 Zone 内
  20. // There is no reason why a nested stack frame must be
  21. // a child of parent stack frame zone.
  22. // This is how one can "escape" a zone.
  23. expect(Zone.current).toBe(rootZone);
  24. expect(Error.captureStackTrace()).toEqual(innerLocation)
  25. });
  26. });
  27. }
  28. 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。下面是上方代码的调用栈的输出。
  1. outerLocation:
  2. at fnOuter()[zoneA];
  3. at Zone.prototype.run()[<root> -> zoneA]
  4. at main()[<root>]
  5. at <anonymous>()[<root>]
  1. innerLocation:
  2. at fnInner()[<root>];
  3. at Zone.prototype.run()[zoneA -> <root>]
  4. at fnOuter()[zoneA];
  5. at Zone.prototype.run()[<root> -> zoneA]
  6. at main()[<root>]
  7. at <anonymous>()[<root>]

第二节:追踪异步操作

我们已经看过如何进入和离开 zone,现在看看 zone 在异步任务中是如何维持的。

  1. let rootZone = Zone.current;
  2. let zoneA = rootZone.fork({name: 'A'});
  3. expect(Zone.current).toBe(rootZone);
  4. // 此处调用 setTimeout 的 zone 为 rootZone
  5. expect(Error.captureStackTrace()).toEqual(rootLocation)
  6. setTimeout(function timeoutCb1() {
  7. // 回调函数也是在 rootZone 中执行
  8. expect(Zone.current).toEqual(rootZone);
  9. expect(Error.captureStackTrace()).toEqual(rootLocationRestored)
  10. }, 0);
  11. zoneA.run(function run1() {
  12. expect(Zone.current).toEqual(zoneA);
  13. // 此处调用 setTimeout 的 zone 为 zoneA
  14. expect(Error.captureStackTrace()).toEqual(zoneALocation)
  15. setTimeout(function timeoutCb2() {
  16. // 回调函数也是在 zoneA 中执行
  17. expect(Zone.current).toEqual(zoneA);
  18. expect(Error.captureStackTrace()).toEqual(zoneALocationRestored)
  19. }, 0);
  20. });
  1. rootLocation:
  2. at <anonymous>()[rootZone]
  3. rootLocationRestored:
  4. at timeoutCb1()[rootZone]
  5. at Zone.prototype.run()[<root> -> <root>]
  6. zoneALocation:
  7. at run1()[zoneA]
  8. at Zone.prototype.run()[<root> -> zoneA]
  9. at <anonymous>()[rootZone]
  10. zoneALocationRestored:
  11. at timeoutCb2()[zoneA]
  12. at Zone.prototype.run()[<root> -> zoneA]

重点:

当异步任务执行的时候,回调函数会在调用异步任务时所处的 zone 中执行,这允许 zone 在异步任务调用时追踪任务。
使用 Promise 时类似的例子(Promise 有些许不同,因为它使用自身的回调处理错误)

  1. let rootZone = Zone.current;
  2. // LibZone 代表不受开发者的一些第三方库
  3. // 本次例子不想大多数第三方库有细粒度的 zone 控制
  4. let libZone = rootZone.fork({name: 'libZone'});
  5. // 代表受开发者控制的 Zone
  6. let appZone = rootZone.fork({name: 'appZone'});
  7. let appZone1 = rootZone.fork({name: 'appZone1'});
  8. // 这个例子是为了演示 promise 的执行和回调的区别
  9. // Promise 的执行可能会出现在第三方库的 zone 中
  10. let promise = libZone.run(() => {
  11. return new Promise((resolve, reject) => {
  12. expect(Zone.current).toBe(libZone);
  13. // Promise 可以立即 resolve 或者像此例一样在一段时间后 resolve
  14. setTimeout(() => {
  15. expect(Zone.current).toBe(libZone);
  16. // Promise 是在 libZone 中 resolve 的,但是不影响监听
  17. resolve('OK');
  18. }, 500);
  19. });
  20. });
  21. appZone.run(() => {
  22. promise.then(() => {
  23. // 由于开发者控制 then 方法的执行位置
  24. // 因此回调会在相同 zone 执行
  25. // 在这个例子中,即为 appZone
  26. expect(Zone.current).toBe(appZone);
  27. });
  28. });
  29. appZone1.run(() => {
  30. promise.then(() => {
  31. // 不同的 then 回调可以在不同的 zone 中调用
  32. expect(Zone.current).toBe(appZone1);
  33. });
  34. });

重点:

  • 对于 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 的底层机制,例子会非常简单。)

  1. // 保存原生 setTimeout 的引用
  2. let originalSetTimeout = window.setTimeout;
  3. // 使用包装了 zone 回调的方法重写 API。
  4. window.setTimeout = function(callback, delay) {
  5. // 调用原生 API 但是包装了 zone 的回调
  6. return originalSetTimeout(
  7. // Wrap the callback method
  8. Zone.current.wrap(callback),
  9. delay
  10. );
  11. }
  12. // 返回一个包装过后的回调版本来恢复 zone。
  13. Zone.prototype.wrap = function(callback) {
  14. // 记录当前 zone
  15. let capturedZone = this;
  16. // 返回一个在当前 zone 中执行源闭包的闭包。
  17. return function() {
  18. // 在记录下的当前 zone 中执行源回调
  19. return capturedZone.runGuarded(callback, this, arguments);
  20. };
  21. };

重点:

  • zone 只进行一次猴子补丁(monkey patch)
  • 进入/离开 zone 只需要改变 Zone.current 的值(不需要额外的猴子补丁)
  • Zone.prototype.wrap 方法是为包装回调提供便利。(包装后的回调函数是在 Zone.prototype.runGuarded() 中执行的),(注:此处的 Zone.prototype.wrap 只是为了方便使用,当前的 zone 通过创建任务来给所有异步方法打补丁)
  • Zone.prototype.runGuarded()Zone.prototype.run() 类似,只不过多了额外的 try-catch 语句来捕获错误。
  1. // 保存原生 Promise.prototype.then 的引用。
  2. let originalPromiseThen = Promise.prototype.then;
  3. // 使用包装了参数的函数重写 API
  4. // 注:真实的 API 具有更多参数,这里只是一个简单的演示版本
  5. Promise.prototype.then = function(callback) {
  6. // 记录当前 zone
  7. let capturedZone = Zone.current;
  8. // 返回一个在当前 zone 中执行源闭包的闭包
  9. function wrappedCallback() {
  10. return capturedZone.run(callback, this, arguments);
  11. };
  12. // 在记录下的当前 zone 中执行源回调。
  13. return originalPromiseThen.call(this, [wrappedCallback]);
  14. };

重点:

  • Promise 使用他们自带的错误处理流程来捕获错误,因此无法使用 Zone.prototype.wrap() 。(我们也可以使用其他 API 来解决这个问题,但是 Promise 是例外规则,因此我们觉得把他的 API 修改为自己的版本是不合理的。)
  • Promise API 不想上面例子中那样清晰明确,他更加庞大。
  • Zone 使用基于微任务的猴子补丁实现了 ZoneAwarePromise,所以本例中的包装只是为了展示概念。