From 0e28297e68baec89ecc4d17bcc4e6ef438c1dbad Mon Sep 17 00:00:00 2001 From: yjbanov Date: Fri, 10 Jul 2015 15:12:21 -0700 Subject: [PATCH] feat(zone): add "on event done" zone hook --- modules/angular2/src/core/application.ts | 2 +- .../src/core/life_cycle/life_cycle.ts | 3 +- modules/angular2/src/core/zone/ng_zone.dart | 63 ++++-- modules/angular2/src/core/zone/ng_zone.ts | 53 ++++-- .../angular2/test/core/zone/ng_zone_spec.ts | 179 ++++++++++++------ 5 files changed, 209 insertions(+), 91 deletions(-) diff --git a/modules/angular2/src/core/application.ts b/modules/angular2/src/core/application.ts index 7554390854..b4333e372e 100644 --- a/modules/angular2/src/core/application.ts +++ b/modules/angular2/src/core/application.ts @@ -146,7 +146,7 @@ function _createNgZone(givenReporter: Function): NgZone { var reporter = isPresent(givenReporter) ? givenReporter : defaultErrorReporter; var zone = new NgZone({enableLongStackTrace: assertionsEnabled()}); - zone.initCallbacks({onErrorHandler: reporter}); + zone.overrideOnErrorHandler(reporter); return zone; } diff --git a/modules/angular2/src/core/life_cycle/life_cycle.ts b/modules/angular2/src/core/life_cycle/life_cycle.ts index 5a9e218367..22c34d80f1 100644 --- a/modules/angular2/src/core/life_cycle/life_cycle.ts +++ b/modules/angular2/src/core/life_cycle/life_cycle.ts @@ -57,7 +57,8 @@ export class LifeCycle { this._changeDetector = changeDetector; } - zone.initCallbacks({onErrorHandler: this._errorHandler, onTurnDone: () => this.tick()}); + zone.overrideOnErrorHandler(this._errorHandler); + zone.overrideOnTurnDone(() => this.tick()); } /** diff --git a/modules/angular2/src/core/zone/ng_zone.dart b/modules/angular2/src/core/zone/ng_zone.dart index 0d1045cf4b..b755d38fd3 100644 --- a/modules/angular2/src/core/zone/ng_zone.dart +++ b/modules/angular2/src/core/zone/ng_zone.dart @@ -3,6 +3,9 @@ library angular.zone; import 'dart:async'; import 'package:stack_trace/stack_trace.dart' show Chain; +typedef void ZeroArgFunction(); +typedef void ErrorHandlingFn(error, stackTrace); + /** * A `Zone` wrapper that lets you schedule tasks after its private microtask queue is exhausted but * before the next "VM turn", i.e. event loop iteration. @@ -19,9 +22,10 @@ import 'package:stack_trace/stack_trace.dart' show Chain; * instantiated. The default `onTurnDone` runs the Angular change detection. */ class NgZone { - Function _onTurnStart; - Function _onTurnDone; - Function _onErrorHandler; + ZeroArgFunction _onTurnStart; + ZeroArgFunction _onTurnDone; + ZeroArgFunction _onEventDone; + ErrorHandlingFn _onErrorHandler; // Code executed in _mountZone does not trigger the onTurnDone. Zone _mountZone; @@ -65,21 +69,40 @@ class NgZone { } /** - * Initializes the zone hooks. - * - * The given error handler should re-throw the passed exception. Otherwise, exceptions will not - * propagate outside of the [NgZone] and can alter the application execution flow. - * Not re-throwing could be used to help testing the code or advanced use cases. - * - * @param {Function} onTurnStart called before code executes in the inner zone for each VM turn - * @param {Function} onTurnDone called at the end of a VM turn if code has executed in the inner zone - * @param {Function} onErrorHandler called when an exception is thrown by a macro or micro task + * Sets the zone hook that is called just before Angular event turn starts. + * It is called once per browser event. */ - void initCallbacks( - {Function onTurnStart, Function onTurnDone, Function onErrorHandler}) { - _onTurnStart = onTurnStart; - _onTurnDone = onTurnDone; - _onErrorHandler = onErrorHandler; + void overrideOnTurnStart(ZeroArgFunction onTurnStartFn) { + this._onTurnStart = onTurnStartFn; + } + + /** + * Sets the zone hook that is called immediately after Angular processes + * all pending microtasks. + */ + void overrideOnTurnDone(ZeroArgFunction onTurnDoneFn) { + this._onTurnDone = onTurnDoneFn; + } + + /** + * Sets the zone hook that is called immediately after the last turn in the + * current event completes. At this point Angular will no longer attempt to + * sync the UI. Any changes to the data model will not be reflected in the + * DOM. {@link onEventDoneFn} is executed outside Angular zone. + * + * This hook is useful for validating application state (e.g. in a test). + */ + void overrideOnEventDone(ZeroArgFunction onEventDoneFn) { + this._onEventDone = onEventDoneFn; + } + + /** + * Sets the zone hook that is called when an error is uncaught in the + * Angular zone. The first argument is the error. The second argument is + * the stack trace. + */ + void overrideOnErrorHandler(ErrorHandlingFn errorHandlingFn) { + this._onErrorHandler = errorHandlingFn; } /** @@ -150,7 +173,11 @@ class NgZone { // Trigger onTurnDone at the end of a turn if _innerZone has executed some code try { _inVmTurnDone = true; - parent.run(_innerZone, _onTurnDone); + parent.run(_innerZone, _onTurnDone); + + if (_pendingMicrotasks == 0 && _onEventDone != null) { + runOutsideAngular(_onEventDone); + } } finally { _inVmTurnDone = false; _hasExecutedCodeInInnerZone = false; diff --git a/modules/angular2/src/core/zone/ng_zone.ts b/modules/angular2/src/core/zone/ng_zone.ts index 304a7d7fb0..a23fc9c2fc 100644 --- a/modules/angular2/src/core/zone/ng_zone.ts +++ b/modules/angular2/src/core/zone/ng_zone.ts @@ -23,6 +23,7 @@ export class NgZone { _onTurnStart: () => void; _onTurnDone: () => void; + _onEventDone: () => void; _onErrorHandler: (error, stack) => void; // Number of microtasks pending from _innerZone (& descendants) @@ -53,6 +54,7 @@ export class NgZone { constructor({enableLongStackTrace}) { this._onTurnStart = null; this._onTurnDone = null; + this._onEventDone = null; this._onErrorHandler = null; this._pendingMicrotasks = 0; @@ -70,22 +72,40 @@ export class NgZone { } /** - * Initializes the zone hooks. - * - * @param {() => void} onTurnStart called before code executes in the inner zone for each VM turn - * @param {() => void} onTurnDone called at the end of a VM turn if code has executed in the inner - * zone - * @param {(error, stack) => void} onErrorHandler called when an exception is thrown by a macro or - * micro task + * Sets the zone hook that is called just before Angular event turn starts. + * It is called once per browser event. */ - initCallbacks({onTurnStart, onTurnDone, onErrorHandler}: { - onTurnStart?: /*() => void*/ Function, - onTurnDone?: /*() => void*/ Function, - onErrorHandler?: /*(error, stack) => void*/ Function - } = {}) { - this._onTurnStart = normalizeBlank(onTurnStart); - this._onTurnDone = normalizeBlank(onTurnDone); - this._onErrorHandler = normalizeBlank(onErrorHandler); + overrideOnTurnStart(onTurnStartFn: Function): void { + this._onTurnStart = normalizeBlank(onTurnStartFn); + } + + /** + * Sets the zone hook that is called immediately after Angular processes + * all pending microtasks. + */ + overrideOnTurnDone(onTurnDoneFn: Function): void { + this._onTurnDone = normalizeBlank(onTurnDoneFn); + } + + /** + * Sets the zone hook that is called immediately after the last turn in the + * current event completes. At this point Angular will no longer attempt to + * sync the UI. Any changes to the data model will not be reflected in the + * DOM. {@link onEventDoneFn} is executed outside Angular zone. + * + * This hook is useful for validating application state (e.g. in a test). + */ + overrideOnEventDone(onEventDoneFn: Function): void { + this._onEventDone = normalizeBlank(onEventDoneFn); + } + + /** + * Sets the zone hook that is called when an error is uncaught in the + * Angular zone. The first argument is the error. The second argument is + * the stack trace. + */ + overrideOnErrorHandler(errorHandlingFn: Function): void { + this._onErrorHandler = normalizeBlank(errorHandlingFn); } /** @@ -172,6 +192,9 @@ export class NgZone { try { this._inVmTurnDone = true; parentRun.call(ngZone._innerZone, ngZone._onTurnDone); + if (ngZone._pendingMicrotasks === 0 && isPresent(ngZone._onEventDone)) { + ngZone.runOutsideAngular(ngZone._onEventDone); + } } finally { this._inVmTurnDone = false; ngZone._hasExecutedCodeInInnerZone = false; diff --git a/modules/angular2/test/core/zone/ng_zone_spec.ts b/modules/angular2/test/core/zone/ng_zone_spec.ts index d581e37f3a..1e850ab5d9 100644 --- a/modules/angular2/test/core/zone/ng_zone_spec.ts +++ b/modules/angular2/test/core/zone/ng_zone_spec.ts @@ -46,7 +46,8 @@ export function main() { function createZone(enableLongStackTrace) { var zone = new NgZone({enableLongStackTrace: enableLongStackTrace}); - zone.initCallbacks({onTurnStart: _log.fn('onTurnStart'), onTurnDone: _log.fn('onTurnDone')}); + zone.overrideOnTurnStart(_log.fn('onTurnStart')); + zone.overrideOnTurnDone(_log.fn('onTurnDone')); return zone; } @@ -63,7 +64,7 @@ export function main() { it('should produce long stack traces', inject([AsyncTestCompleter], (async) => { macroTask(() => { - _zone.initCallbacks({onErrorHandler: logError}); + _zone.overrideOnErrorHandler(logError); var c = PromiseWrapper.completer(); _zone.run(() => { @@ -86,7 +87,7 @@ export function main() { it('should produce long stack traces (when using microtasks)', inject([AsyncTestCompleter], (async) => { macroTask(() => { - _zone.initCallbacks({onErrorHandler: logError}); + _zone.overrideOnErrorHandler(logError); var c = PromiseWrapper.completer(); _zone.run(() => { @@ -114,7 +115,7 @@ export function main() { it('should disable long stack traces', inject([AsyncTestCompleter], (async) => { macroTask(() => { - _zone.initCallbacks({onErrorHandler: logError}); + _zone.overrideOnErrorHandler(logError); var c = PromiseWrapper.completer(); _zone.run(() => { @@ -160,6 +161,77 @@ function commonTests() { }); })); + it('should call onEventDone once at the end of event', inject([AsyncTestCompleter], (async) => { + // The test is set up in a way that causes the zone loop to run onTurnDone twice + // then verified that onEventDone is only called once at the end + _zone.overrideOnTurnStart(null); + _zone.overrideOnEventDone(() => { _log.add('onEventDone'); }); + + var times = 0; + _zone.overrideOnTurnDone(() => { + times++; + _log.add(`onTurnDone ${times}`); + if (times < 2) { + // Scheduling a microtask causes a second digest + microTask(() => {}); + } + }); + + macroTask(() => { _zone.run(_log.fn('run')); }); + + macroTask(() => { + expect(_log.result()).toEqual('run; onTurnDone 1; onTurnDone 2; onEventDone'); + async.done(); + }); + })); + + it('should not allow onEventDone to cause further digests', + inject([AsyncTestCompleter], (async) => { + _zone.overrideOnTurnStart(null); + + var eventDone = false; + _zone.overrideOnEventDone(() => { + if (eventDone) throw 'Should not call this more than once'; + _log.add('onEventDone'); + // If not implemented correctly, this microtask will cause another digest, + // which is not what we want. + microTask(() => {}); + eventDone = true; + }); + + _zone.overrideOnTurnDone(() => { _log.add('onTurnDone'); }); + + macroTask(() => { _zone.run(_log.fn('run')); }); + + macroTask(() => { + expect(_log.result()).toEqual('run; onTurnDone; onEventDone'); + async.done(); + }); + })); + + it('should run async tasks scheduled inside onEventDone outside Angular zone', + inject([AsyncTestCompleter], (async) => { + _zone.overrideOnTurnStart(null); + + _zone.overrideOnEventDone(() => { + _log.add('onEventDone'); + // If not implemented correctly, this time will cause another digest, + // which is not what we want. + TimerWrapper.setTimeout(() => { _log.add('asyncTask'); }, 5); + }); + + _zone.overrideOnTurnDone(() => { _log.add('onTurnDone'); }); + + macroTask(() => { _zone.run(_log.fn('run')); }); + + macroTask(() => { + TimerWrapper.setTimeout(() => { + expect(_log.result()).toEqual('run; onTurnDone; onEventDone; asyncTask'); + async.done(); + }, 10); + }); + })); + it('should call onTurnStart once before a turn and onTurnDone once after the turn', inject([AsyncTestCompleter], (async) => { @@ -201,12 +273,11 @@ function commonTests() { it('should not run onTurnStart and onTurnDone for nested Zone.run invoked from onTurnDone', inject([AsyncTestCompleter], (async) => { - _zone.initCallbacks({ - onTurnDone: () => { - _log.add('onTurnDone:started'); - _zone.run(() => _log.add('nested run')); - _log.add('onTurnDone:finished'); - } + _zone.overrideOnTurnStart(null); + _zone.overrideOnTurnDone(() => { + _log.add('onTurnDone:started'); + _zone.run(() => _log.add('nested run')); + _log.add('onTurnDone:finished'); }); macroTask(() => { _zone.run(() => { _log.add('start run'); }); }); @@ -306,19 +377,17 @@ function commonTests() { 'onTurnDone after executing the task', inject([AsyncTestCompleter], (async) => { var ran = false; - _zone.initCallbacks({ - onTurnStart: _log.fn('onTurnStart'), - onTurnDone: () => { - _log.add('onTurnDone(begin)'); - if (!ran) { - microTask(() => { - ran = true; - _log.add('executedMicrotask'); - }); - } - - _log.add('onTurnDone(end)'); + _zone.overrideOnTurnStart(_log.fn('onTurnStart')); + _zone.overrideOnTurnDone(() => { + _log.add('onTurnDone(begin)'); + if (!ran) { + microTask(() => { + ran = true; + _log.add('executedMicrotask'); + }); } + + _log.add('onTurnDone(end)'); }); macroTask(() => { _zone.run(_log.fn('run')); }); @@ -338,19 +407,17 @@ function commonTests() { 'a scheduleMicrotask in run', inject([AsyncTestCompleter], (async) => { var ran = false; - _zone.initCallbacks({ - onTurnStart: _log.fn('onTurnStart'), - onTurnDone: () => { - _log.add('onTurnDone(begin)'); - if (!ran) { - _log.add('onTurnDone(scheduleMicrotask)'); - microTask(() => { - ran = true; - _log.add('onTurnDone(executeMicrotask)'); - }); - } - _log.add('onTurnDone(end)'); + _zone.overrideOnTurnStart(_log.fn('onTurnStart')); + _zone.overrideOnTurnDone(() => { + _log.add('onTurnDone(begin)'); + if (!ran) { + _log.add('onTurnDone(scheduleMicrotask)'); + microTask(() => { + ran = true; + _log.add('onTurnDone(executeMicrotask)'); + }); } + _log.add('onTurnDone(end)'); }); macroTask(() => { @@ -376,25 +443,23 @@ function commonTests() { var donePromiseRan = false; var startPromiseRan = false; - _zone.initCallbacks({ - onTurnStart: () => { - _log.add('onTurnStart(begin)'); - if (!startPromiseRan) { - _log.add('onTurnStart(schedulePromise)'); - microTask(_log.fn('onTurnStart(executePromise)')); - startPromiseRan = true; - } - _log.add('onTurnStart(end)'); - }, - onTurnDone: () => { - _log.add('onTurnDone(begin)'); - if (!donePromiseRan) { - _log.add('onTurnDone(schedulePromise)'); - microTask(_log.fn('onTurnDone(executePromise)')); - donePromiseRan = true; - } - _log.add('onTurnDone(end)'); + _zone.overrideOnTurnStart(() => { + _log.add('onTurnStart(begin)'); + if (!startPromiseRan) { + _log.add('onTurnStart(schedulePromise)'); + microTask(_log.fn('onTurnStart(executePromise)')); + startPromiseRan = true; } + _log.add('onTurnStart(end)'); + }); + _zone.overrideOnTurnDone(() => { + _log.add('onTurnDone(begin)'); + if (!donePromiseRan) { + _log.add('onTurnDone(schedulePromise)'); + microTask(_log.fn('onTurnDone(executePromise)')); + donePromiseRan = true; + } + _log.add('onTurnDone(end)'); }); macroTask(() => { @@ -507,7 +572,7 @@ function commonTests() { it('should call the on error callback when it is defined', inject([AsyncTestCompleter], (async) => { macroTask(() => { - _zone.initCallbacks({onErrorHandler: logError}); + _zone.overrideOnErrorHandler(logError); var exception = new BaseException('sync'); @@ -520,7 +585,7 @@ function commonTests() { })); it('should call onError for errors from microtasks', inject([AsyncTestCompleter], (async) => { - _zone.initCallbacks({onErrorHandler: logError}); + _zone.overrideOnErrorHandler(logError); var exception = new BaseException('async'); @@ -537,7 +602,8 @@ function commonTests() { inject([AsyncTestCompleter], (async) => { var exception = new BaseException('fromOnTurnDone'); - _zone.initCallbacks({onErrorHandler: logError, onTurnDone: () => { throw exception; }}); + _zone.overrideOnErrorHandler(logError); + _zone.overrideOnTurnDone(() => { throw exception; }); macroTask(() => { _zone.run(() => {}); }); @@ -554,7 +620,8 @@ function commonTests() { var exception = new BaseException('fromOnTurnDone'); - _zone.initCallbacks({onErrorHandler: logError, onTurnDone: () => { throw exception; }}); + _zone.overrideOnErrorHandler(logError); + _zone.overrideOnTurnDone(() => { throw exception; }); macroTask(() => { _zone.run(() => { microTask(() => { asyncRan = true; }); }); });