Zone 传播

第一章介绍如何创建 Zones,并通过异步函数传播

Entering Zones, Forking and Stack Frames

首先要了解的是如何创建 zone(forked),如何通过系统传播,以及如何形象的理解 zones 和堆栈帧。

// RootZone 环境的所以它难以与无 zone 区分。
let rootZone = Zone.current;
// We create a new zone by forking an existing zone.
let zoneA = rootZone.fork({name: 'zoneA'});
// Each zone has a name for debugging
expect(rootZone.name).toEqual('<root>');
expect(zoneA.name).toEqual('zoneA');
// Child zone knows about its parent zone. (one way reference)
expect(zoneA.parent).toBe(rootZone);
function main() {
    // zones can be entered/exited using the `run` method only.
    zoneA.run(function fnOuter() {
        // inside the `run` method the Zone.current has been updated
        expect(Zone.current).toBe(zoneA);
        // Mental model: Each stack frame is associated with a zone
        expect(Error.captureStackTrace()).toEqual(outerLocation)
        // Zones are nested in the same way that stack frames are nested.
        rootZone.run(function fnInner() {
            // 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.prototype.run()。
  • 一个给定的堆栈帧正好有一个与之关联的区域。 上方或下方的堆栈帧必须具有相同的区域,除非该堆栈帧是 Zone.prototype.run()。
  • 子区域有对父区域的引用(但父区域没有对子区域的引用)具有父关系仅允许通过简单地释放对区域的引用来对区域进行垃圾收集。

Stack Frames

重要的是要了解给定的堆栈帧只能与一个区域相关联。 (即,函数的前半部分不可能在与函数的后半部分不同的区域中运行。同一个函数可能在不同的调用中具有不同的区域)。 只有通过进入或退出 Zone.prototype.run() 才能进入或离开 Zone。 区域更新堆栈跟踪以显示区域以获得更好的可见性。 下面是来自上述示例的两个堆栈快照,它们显示了每个堆栈帧的关联区域。

重要的是要理解,给定的堆栈帧只能与一个区域相关联。 (即,函数的前半部分不可能在与函数的后半部分不同的区域中运行。相同的函数可能在不同的调用上具有不同的区域)。 只能通过输入或退出Zone.prototype.run()来输入或留下区域。 区域更新堆栈跟踪以显示区域,以提高可见性。 下面是来自上面示例的两个堆栈快照,其示出了每个堆栈帧的关联区域。

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>]

跟踪异步操作

我们已经看到了如何进入和退出 zones,现在让我们看看我们如何跟踪跨异步操作的 zones 。

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]

关键点:

  • 当调度异步工作时,回调函数将在与调用异步API时存在的相同 zone 中执行。 这允许跨越许多异步调用来跟踪 zone。

类似示例使用promises。(Promises有点不同,因为它们在回调中处理自己的异常)

let rootZone = Zone.current;
// LibZone表示一些第三方库,开发人员无法控制。 这个zone仅用于说明目的。
// 在实践中,大多数第三方库不可能有这种细粒度zone控制。
let libZone = rootZone.fork({name: 'libZone'});
// Represents a zone of an application which the developer does
// control
let appZone = rootZone.fork({name: 'appZone'});
// In this Example we try to demonstrate the difference between
// resolving a promise and listening to a promise. Promise
// resolution could happen in a third party libZone.
let promise = libZone.run(() => {
  return new Promise((resolve, reject) => {
    expect(Zone.current).toBe(libZone);
    // The Promise can be resolved immediately or at some later
    // point in time as in this example.
    setTimeout(() => {
      expect(Zone.current).toBe(libZone);
      // Promise is resolved in libZone, but this does not affect
      // the promise listeners.
      resolve('OK');
    }, 500);
  });
});
appZone.run(() => {
  promise.then(() => {
    // Because the developer controls the in which zone .then()
    // executes, they will expect that the callback will execute
    // the same zone, in this case the appZone.
    expect(Zone.current).toBe(appZone);
  });
});

关键点:

  • 对于Promise,在.then() 调用生效时 thenCallback 会在 zone 中被调用 。
    • 或者,我们可以为thenCallback使用不同的 zone ,例如Promise创建 zone 或Promise解析 zone 。 这两者都不是一个好的匹配,因为一个Promise可以在一个第三方库中创建和解析,它可以有自己的 zone 。 然后将所得到的承诺传递到在其自己的 zone 中运行的应用程序中。 如果应用程序在其自己的 zone 中注册.then(),那么它将期望它自己的 zone 传播。 示例:调用fetch()返回一个promise。 内部fetch()可能使用自己的 zone 为自己的原因。 调用.then()的应用程序将期望应用程序 zone 。 (我们不希望fetch() zone 泄漏到我们的应用程序中。)

异步方法补丁

这里我们将看看如何通过异步回调传播zone的基本机制。 (注意:实际工作情况有点复杂,因为它们通过稍后讨论的任务调度机制进行调度,为了清楚起见,该示例进行了简化)。

// 保存原始的setTimeout引用
let originalSetTimeout = window.setTimeout;
// Overwrite the API with a function which wraps callback in zone.
window.setTimeout = function(callback, delay) {
  // Invoke the original API but wrap the callback in zone.
  return originalSetTimeout(
    // Wrap the callback method
    Zone.current.wrap(callback),
    delay
  );
}
// Return a wrapped version of the callback which restores zone.
Zone.prototype.wrap[c] = function(callback) {
  // Capture the current zone
  let capturedZone = this;
  // Return a closure which executes the original closure in zone.
  return function() {
    // Invoke the original callback in the captured zone.
    return capturedZone.runGuarded(callback, this, arguments);
  };
};

关键点:

  • Zones 的猴子补丁方法只修补一次。
  • 进入/离开 zone 只需更改Zone.current的值。(不需要进一步的猴子补丁)
  • Zone.prototype.wrap 方法为包装回调提供了便利。 (包装好的回调通过 Zone.prototype.runGuarded() 执行)
  • Zone.prototype.runGuarded() 与 Zone.prototype.run() 类似,但有额外的 try-catch 块用于处理稍后描述的异常
// Save the original reference to Promise.prototype.then.
let originalPromiseThen = Promise.prototype.then;
// Overwrite the API with function which wraps the arguments
// NOTE: this is simplified as actual API has more arguments.
Promise.prototype.then = function(callback) {
  // Capture the current zone
  let capturedZone = Zone.current;
  // Return a closure which executes the original closure in zone.
  function wrappedCallback() {
    return capturedZone.run(callback, this, arguments);
  };
  // Invoke the original callback in the captured zone.
  return originalPromiseThen.call(this, [wrappedCallback]);
};

关键点:

  • Promise 处理它们自己的异常,因此它们不能使用 Zone.prototype.wrap()。 (我们可以有单独的 API,但 Promise 是规则的例外,所以我觉得没有理由创建自己的 API。)
  • Promise API较多,在此示例中未显式显示。