到目前为止我们已经知道了 zone 如何创建和传播的,但是我们依旧没有讨论过 zone 提供了什么值。为了完整发挥 zone 的用处,zone 需要提供修改不同 zone 方法的功能。这些功能有:

  • 父 zone 相互组合的功能
  • 任务执行的可观察性
  • 同意的错误处理

第一节:ZoneDelegate 的例子

拦截 zone 事件是很难处理的,因为 zone 在运行时生成,而例如子类化和猴子补丁等一般方法只能在已知父 zone 时正常工作。为了更好地演示这个问题,让我们假设我们想通过拦截 run() 事件来统计执行时间和输出 zone。
这是一个无法正常工作的演示版本。

  1. // 在设计阶段,我们无法判断哪个 zone 将会作为父 zone
  2. // 因此,必须将父 zone 作为参数传入 constructor
  3. class TimingZone extends Zone {
  4. constructor(parent) { super(parent, 'timingZone');}
  5. // 我们需要打断 run 然后重写它
  6. run() {
  7. // 记录开始运行的时间
  8. var start = performance.now();
  9. // 看起来应该在此处调用 super.run()
  10. // 但是 super.run() 内部必须调用 parent.run
  11. // 所以重点不在 super.run ,我们更应该关注 parent.run()
  12. // 看接下来的例子
  13. super.run(...arguments);
  14. // 记录结束运行的时间
  15. var end = performance.now();
  16. // 输出运行时间和当前 zone
  17. console.log(this.name, 'Duration:', end - start);
  18. }
  19. }
  20. // 在设计阶段,我们无法判断哪个 zone 将会作为父 zone
  21. // 因此,必须将父 zone 作为参数传入 constructor
  22. class LogZone extends Zone {
  23. constructor(parent) { super(parent, 'logZone');}
  24. run() {
  25. // 打印 zone 的 name 属性和 “enter”
  26. console.log(this.name, 'enter');
  27. // 调用 parent.run 的问题在于这会导致当前 zone 的 current 值变成父 zone
  28. // 我们需要找到一个不改变 current 的调用父 zone 的 run 方法的途径。(译注:原文本句为粗体字)
  29. this.parent.run.apply(this, arguments);
  30. // 打印 zone 的 name 属性和 “leave”
  31. console.log(this.name, 'leave');
  32. }
  33. }

我们使用上面加工后的 Zone 来写几个简单的例子

  1. // 综合多个 zone
  2. let rootZone = Zone.current;
  3. let timingZone = new TimingZone(rootZone);
  4. let logZone = new LogZone(timingZone);
  5. logZone.run(() => {
  6. console.log(Zone.current.name, 'Hello World!');
  7. });

这是我们期待的输出。我们希望输出的当前 zone 是 logZone

  1. logZone enter
  2. logZone Hello World;
  3. logZone Duration: 0.123
  4. logZone leave

而这是上述例子的真实输出

  1. logZone enter
  2. rootZone Hello World;
  3. timingZone Duration: 0.123
  4. logZone leave

我们注意到每个 zone 都输出了他自己的 name 属性(而不是源 zone 的),因此最终这些代码是运行在rootZone 中而不是 logZone 中。

为什么一般方法没有生效

一般的重写方法(super 或者 parent)没有生效的原因是父 zone 无法在设计时就被预测。如果我们在设计时就知道父 zone 的话,我们可以这么写:

  1. class TimingZone extends RootZone {
  2. run() {
  3. super.run(...arguments);
  4. }
  5. }
  6. class LogZone extends TimingZone {
  7. run() {
  8. super.run(...arguments);
  9. }
  10. }

如果我们能在设计时就决定好 zone 的层级, super.run() 的调用就会如预期一般工作。
然而,由于父 zone 只有在代码运行时才能被知晓,我们需要把改变 zone 的流程从调用上层方法的过程中抽离出来。为了解决这个问题,这个钩子方法已经被包含在 fork 方法中,并且这个钩子方法还接收一个参数,这个参数是其父 zone 的代理(只会调用钩子方法),而不是其父 zone(调用它的 run 方法会导致 current 值变化)。

Zone ZoneDelegate 描述
run invoke 执行 run() 内的语句时,会调用 ZoneDelegate 上的 invoke() 钩子,这允许在不改变当前 zone 的前提下代理钩子
wrap intercept 执行 wrap() 内的语句时,会调用 ZoneDelegate 上的 intercept() 钩子,这允许在不重新 wrap 的前提下代理钩子

译注: 一般我们想修改某个库的内置方法,都是通过 保存内部方法,修改,重新调用并传入 this 的流程。 但是 zonejs 的库用这种流程并不奏效。 因此 zonejs 官方的 fork 方法就已经自带了几个可以让你 hook 的生命周期参数,你把这些方法当作参数传入,这个方法在被调用的时候会接收到来自官方传给你的一个叫做 zone代理 的参数,这个参数把 run 修改为了 invoke,把 wrap 修改为了 intercept,它们之间的唯一区别就是,invoke 在调用的时候不会自动修改 currnet 属性,而是让你显式传入你希望他执行的 zone,intercept 方法也不会重新调用 wrap 而造成重新绑定的问题。 具体例子请看下一节。

第二节:生命周期参数

让我们看看使用 Zone.prototype.fork() API 重写的上面的例子。

  1. let timingZone = Zone.current.fork({
  2. name: 'timingZone',
  3. onInvoke: function(parentZoneDelegate, currentZone, targetZone,
  4. callback, applyThis, applyArgs, source) {
  5. var start = performance.now();
  6. parentZoneDelegate.invoke(
  7. targetZone, callback, applyThis, applyArgs, source);
  8. var end = performance.now();
  9. console.log(
  10. 'Zone:', targetZone.name,
  11. 'Intercepting zone:', currentZone.name,
  12. 'Duration:', end - start
  13. }
  14. };
  15. let logZone = timingZone.fork({
  16. name: 'logZone',
  17. onInvoke: function(parentZoneDelegate, currentZone, targetZone,
  18. callback, applyThis, applyArgs, source) {
  19. console.log(
  20. 'Zone:', targetZone.name,
  21. 'Intercepting zone:', currentZone.name,
  22. 'enter');
  23. parentZoneDelegate.invoke(
  24. targetZone, callback, applyThis, applyArgs, source);
  25. console.log(
  26. 'Zone:', targetZone.name,
  27. 'Intercepting zone:', currentZone.name,
  28. 'leave');
  29. }
  30. });
  31. let appZone = logZone.fork({name: 'appZone'});
  32. appZone.run(function myApp() {
  33. console.log('Zone:', Zone.current.name, 'Hello World!');
  34. });

输出结果为:

  1. Zone: appZone Intercepting zone: logZone enter
  2. Zone: appZone Hello World!
  3. Zone: appZone Intercepting zone: timingZone duration 0.123
  4. Zone: appZone Intercepting zone: logZone leave

重点:

  • 如果父 zone 不确定,继承这种做法就无效。(代理执行父 zone 的 run() 方法会改变 current 属性)
  • ZoneDelegate 允许你再保持 zone 上下文的情况下对生命周期函数 hook。

使用 promise 的例子:

  1. let logZone = Zone.current.fork({
  2. name: 'logZone',
  3. onInvoke: function(parentZoneDelegate, currentZone, targetZone,
  4. callback, delegate, applyThis, applyArgs, source) {
  5. console.log(targetZone.name, 'enter');
  6. parentZoneDelegate.invoke(
  7. targetZone, callback, applyThis, applyArgs, source)
  8. console.log(targetZone.name, 'leave');
  9. }
  10. });
  11. logZone.run(function myApp() {
  12. console.log(Zone.current.name, 'queue promise');
  13. Promise.resolve('OK').then((v) =>
  14. console.log(Zone.current.name, 'Promise', v));
  15. });

输出结果:

  1. logZone enter
  2. logZone queuePromise
  3. logZone leave
  4. logZone enter
  5. logZone Promise OK
  6. logZone leave

重点:

  • zone 进入了两次,一次是执行 promise,一次是 promise 的 then 方法。

第三节:错误处理

译注:原文这里就没有内容