案例 1.a: 测试 - 在测试中禁止异步代码

我们都希望能快速连续地进行测试。一种实现方法就是全部使用同步测试,因为这种测试快速并且行为可预测。即使使用侵入式 mock 也不是所有测试都可以完全变为同步操作。重要的是需要有一种方法可以把测试标记为同步测试并且强制进行同步测试,而且随着时间的推移不会意外地出现异步行为。

  1. var syncZoneSpec = {
  2. name: 'SyncZone',
  3. onScheduleTask: function() {
  4. throw new Error('No Async work is allowed in test.');
  5. }
  6. }
  7. function sync(fn) {
  8. return function(...args) {
  9. Zone.current.fork(new EventReleaseZoneSpec).run(fn, args, this);
  10. }
  11. }
  12. it('should fail when doing async', sync(() => {
  13. Promise.resolve('value');
  14. }));

译注: 第十行应该是 Zone.current.fork(syncZoneSpec).run(fn, args, this);

重点:

  • 再 SyncZone 中执行测试的时候,如果创建一个异步任务,测试将会自动失败。

案例 1.b:测试 - 自动清理事件

测试过程中,对全局变量的操作可能会遗留到下一次测试中。常见的操作时在 DOM 上比如 body 上添加监听事件,这些事件会遗留到下一次测试中。Zone 可以追踪所有监听事件的注册并且在测试完成之后自动清理这些事件。

  1. class EventReleaseZoneSpec {
  2. constructor() {
  3. this.name = 'CleanupZone';
  4. // 在这里持续追踪事件任务
  5. this.eventTasks = [];
  6. }
  7. onScheduleTask(parentZoneDelegate, currentZone,
  8. targetZone, task) {
  9. // 无论何时,只要事件任务被注册,就将他添加到这个列表中
  10. if (task.type == 'eventTask') {
  11. this.eventTasks.push(task);
  12. }
  13. return parentZoneDelegate.scheduleTask(targetZone, task);
  14. }
  15. cleanup() {
  16. // 取消所有任务
  17. while(this.eventTasks.length) {
  18. Zone.current.cancelTask(this.eventTasks.pop());
  19. }
  20. }
  21. }
  22. // 包装测试方法,并在测试完成后自动释放所有监听事件
  23. function cleanup(fn) {
  24. return function(done) {
  25. let zoneSpec = new EventReleaseZoneSpec();
  26. let args = [() => {
  27. zoneSpec.cleanup();
  28. done();
  29. }];
  30. Zone.current.fork(zoneSpec).run(fn, this, args);
  31. }
  32. }
  33. it('should auto-cleanup events, cleanup((done) => {
  34. someElement.addEventListener('click', () => console.log('click'));
  35. someElement.click();
  36. done();
  37. }));

译注:

  1. 37 行缺少一个单引号
  2. 测试中的 done 方法在 cleanup 中被修改,40 行的 done 方法实际执行的是 29 行 args 数组中的第一项的方法。

重点:

测试结束后会自动清理运行时注册的所有监听事件。

案例 1.c:测试 - 自动等待测试完成

当运行异步测试的时候,很难知道何时所有测试全部完成。通常测试用例会等待一个特定的任务完成并且断言为正确的结果。而在异步测试的时候会有很多不同的异步任务,很难知道这些任务何时全部完成。未完成的测试任务有可能会对下一次测试造成不利影响,尤其是如果它们修改了全局状态比如 DOM。我们需要一种能把测试标记为异步,并且在测试全部执行完毕后自动完成的方法。

  1. class TrackTaskZoneSpec {
  2. constructor(done) {
  3. this.name = 'TaskTrackingZone';
  4. this.done = done;
  5. }
  6. onHasTask(delegate, current, target, hasTaskState) {
  7. if (!hasTaskState.microTask && !hasTaskState.macroTask) {
  8. this.done();
  9. }
  10. }
  11. }
  12. function async(fn) {
  13. return function(done) {
  14. Zone.current.fork(new TrackTaskZoneSpec(done)).run(fn);
  15. }
  16. }
  17. it('should auto-wait for async test', async(() => {
  18. setTimeout(() => {
  19. Promise.resolve(0).then(() => {
  20. // 只有在这行代码执行完成后,测试才会结束
  21. console.log('wait for me'));
  22. }
  23. }, 0);
  24. }));

重点:

  • 现在测试会自动等到所有异步任务完成之后才会进行下一个测试。
  • 由于 zone 会自动检测测试何时完成,因此不会出现太早结束测试(调用 done 方法)的问题。这降低了下一个测试影响当前测试的可能性。

案例 2:长调用栈追踪

一般来说,如果一个应用在某个点抛出了一个错误,但是调用栈中不包含任务创建时的上下文,这种错误信息不是特别有用。看看下面这个例子:

  1. Error: ReferenceError: name is not defined
  2. paintResponse()

我们知道这是个错误,但是我们不知道这个任务是何时创建的。长调用栈追踪通过输出一个更长的,跨越任务执行的调用栈来提供了这样一个上下文。

  1. Error: ReferenceError: name is not defined
  2. at paintResponse()
  3. ---- async gap ----
  4. at requestAnimationFrame()
  5. at XMLHttpRequest.resolve()
  6. ---- async gap ----
  7. at XMLHttpRequest.send()
  8. at fetchData()
  9. at clickHandler()
  10. ---- async gap ----
  11. at addEventListener()
  12. at main()

通过长调用栈追踪,使得把 从 event listener 创建,然后点击,然后通过 XHR 获取数据,到最终尝试使用 requestAnimationFrame 渲染但是失败 这些片段联系起来成为可能。

  1. class LongStackTraceZoneSpec {
  2. constructor() {
  3. this.name = 'LongStackTrace';
  4. }
  5. onScheduleTask(parentZoneDelegate, currentZone,
  6. targetZone, task) {
  7. var task = parentZoneDelegate.scheduleTask(targetZone, task);
  8. // 每次任务创建的时候,都将当前调用栈捕获保存下来
  9. task.data.trace = new Error('LongStackTrace');
  10. // 记录创建当前任务的父任务
  11. task.data.parentTask = Zone.currentTask;
  12. return task;
  13. }
  14. onHandleError: function(parentZD, current, target, error) {
  15. error.stack += this.getLongStackTrace();
  16. return parentZD.handleError(target, error);
  17. }
  18. // 把所有调用栈组合起来
  19. getLongStackTrace() {
  20. var trace = [''];
  21. var task = Zone.currentTask;
  22. while(task) {
  23. trace.push(task.data.trace);
  24. task = task.data.parentTask;
  25. }
  26. return trace.join('\n--- async gap --\n');
  27. }
  28. }

重点:

  • 当应用产品把这种调用栈发送回服务器处理时,开发者更容易推测到底发生了什么。

案例 3:框架自动渲染

框架,比如 Angular,需要知道应用何时工作完毕,并在宿主环境渲染之前更新 DOM。在实践中,这意味着框架对主任务和与其相关的微任务执行完毕,并且宿主环境还未接过控制的时间感兴趣。

  1. class VMTurnZoneSpec {
  2. constructor(vmTurnDone) {
  3. this.name = 'VMTurnZone';
  4. this.vmTurnDone = vmTurnDone;
  5. this.hasMicroTask = false
  6. }
  7. onHasTask(delegate, current, target, hasTaskState) {
  8. this.hasMicroTask = hasTaskState.microTask;
  9. if (!this.hasMicroTask) {
  10. this.vmTurnDone();
  11. }
  12. }
  13. onInvokeTask(parent, current, target, task, applyThis, applyArgs){
  14. try {
  15. return parent.invokeTask(target, task, applyThis, applyArgs);
  16. } finally {
  17. if (!this.hasMicroTask) {
  18. this.vmTurnDone();
  19. }
  20. }
  21. }
  22. }

重点:

  • 当测试运行在 SyncZone 中时,如果出现了异步任务,测试会自动失败。

    译注: 这里是错的,应该是复制了上面禁止异步调用的部分下来了 这里的代码是,每当微任务栈执行完毕之后,调用一次传入的 vmTurnDone 方法,可以在这里自动调用框架的渲染方法。

案例 4:追踪用户操作

案例 5:自动释放资源

译注: 原文这里没有内容,应该是未完成。