feat(zone): add "on event done" zone hook

This commit is contained in:
yjbanov 2015-07-10 15:12:21 -07:00
parent 1eebceab27
commit 0e28297e68
5 changed files with 209 additions and 91 deletions

View File

@ -146,7 +146,7 @@ function _createNgZone(givenReporter: Function): NgZone {
var reporter = isPresent(givenReporter) ? givenReporter : defaultErrorReporter; var reporter = isPresent(givenReporter) ? givenReporter : defaultErrorReporter;
var zone = new NgZone({enableLongStackTrace: assertionsEnabled()}); var zone = new NgZone({enableLongStackTrace: assertionsEnabled()});
zone.initCallbacks({onErrorHandler: reporter}); zone.overrideOnErrorHandler(reporter);
return zone; return zone;
} }

View File

@ -57,7 +57,8 @@ export class LifeCycle {
this._changeDetector = changeDetector; this._changeDetector = changeDetector;
} }
zone.initCallbacks({onErrorHandler: this._errorHandler, onTurnDone: () => this.tick()}); zone.overrideOnErrorHandler(this._errorHandler);
zone.overrideOnTurnDone(() => this.tick());
} }
/** /**

View File

@ -3,6 +3,9 @@ library angular.zone;
import 'dart:async'; import 'dart:async';
import 'package:stack_trace/stack_trace.dart' show Chain; 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 * 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. * 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. * instantiated. The default `onTurnDone` runs the Angular change detection.
*/ */
class NgZone { class NgZone {
Function _onTurnStart; ZeroArgFunction _onTurnStart;
Function _onTurnDone; ZeroArgFunction _onTurnDone;
Function _onErrorHandler; ZeroArgFunction _onEventDone;
ErrorHandlingFn _onErrorHandler;
// Code executed in _mountZone does not trigger the onTurnDone. // Code executed in _mountZone does not trigger the onTurnDone.
Zone _mountZone; Zone _mountZone;
@ -65,21 +69,40 @@ class NgZone {
} }
/** /**
* Initializes the zone hooks. * Sets the zone hook that is called just before Angular event turn starts.
* * It is called once per browser event.
* 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
*/ */
void initCallbacks( void overrideOnTurnStart(ZeroArgFunction onTurnStartFn) {
{Function onTurnStart, Function onTurnDone, Function onErrorHandler}) { this._onTurnStart = onTurnStartFn;
_onTurnStart = onTurnStart; }
_onTurnDone = onTurnDone;
_onErrorHandler = onErrorHandler; /**
* 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;
} }
/** /**
@ -151,6 +174,10 @@ class NgZone {
try { try {
_inVmTurnDone = true; _inVmTurnDone = true;
parent.run(_innerZone, _onTurnDone); parent.run(_innerZone, _onTurnDone);
if (_pendingMicrotasks == 0 && _onEventDone != null) {
runOutsideAngular(_onEventDone);
}
} finally { } finally {
_inVmTurnDone = false; _inVmTurnDone = false;
_hasExecutedCodeInInnerZone = false; _hasExecutedCodeInInnerZone = false;

View File

@ -23,6 +23,7 @@ export class NgZone {
_onTurnStart: () => void; _onTurnStart: () => void;
_onTurnDone: () => void; _onTurnDone: () => void;
_onEventDone: () => void;
_onErrorHandler: (error, stack) => void; _onErrorHandler: (error, stack) => void;
// Number of microtasks pending from _innerZone (& descendants) // Number of microtasks pending from _innerZone (& descendants)
@ -53,6 +54,7 @@ export class NgZone {
constructor({enableLongStackTrace}) { constructor({enableLongStackTrace}) {
this._onTurnStart = null; this._onTurnStart = null;
this._onTurnDone = null; this._onTurnDone = null;
this._onEventDone = null;
this._onErrorHandler = null; this._onErrorHandler = null;
this._pendingMicrotasks = 0; this._pendingMicrotasks = 0;
@ -70,22 +72,40 @@ export class NgZone {
} }
/** /**
* Initializes the zone hooks. * Sets the zone hook that is called just before Angular event turn starts.
* * It is called once per browser event.
* @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
*/ */
initCallbacks({onTurnStart, onTurnDone, onErrorHandler}: { overrideOnTurnStart(onTurnStartFn: Function): void {
onTurnStart?: /*() => void*/ Function, this._onTurnStart = normalizeBlank(onTurnStartFn);
onTurnDone?: /*() => void*/ Function, }
onErrorHandler?: /*(error, stack) => void*/ Function
} = {}) { /**
this._onTurnStart = normalizeBlank(onTurnStart); * Sets the zone hook that is called immediately after Angular processes
this._onTurnDone = normalizeBlank(onTurnDone); * all pending microtasks.
this._onErrorHandler = normalizeBlank(onErrorHandler); */
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 { try {
this._inVmTurnDone = true; this._inVmTurnDone = true;
parentRun.call(ngZone._innerZone, ngZone._onTurnDone); parentRun.call(ngZone._innerZone, ngZone._onTurnDone);
if (ngZone._pendingMicrotasks === 0 && isPresent(ngZone._onEventDone)) {
ngZone.runOutsideAngular(ngZone._onEventDone);
}
} finally { } finally {
this._inVmTurnDone = false; this._inVmTurnDone = false;
ngZone._hasExecutedCodeInInnerZone = false; ngZone._hasExecutedCodeInInnerZone = false;

View File

@ -46,7 +46,8 @@ export function main() {
function createZone(enableLongStackTrace) { function createZone(enableLongStackTrace) {
var zone = new NgZone({enableLongStackTrace: 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; return zone;
} }
@ -63,7 +64,7 @@ export function main() {
it('should produce long stack traces', inject([AsyncTestCompleter], (async) => { it('should produce long stack traces', inject([AsyncTestCompleter], (async) => {
macroTask(() => { macroTask(() => {
_zone.initCallbacks({onErrorHandler: logError}); _zone.overrideOnErrorHandler(logError);
var c = PromiseWrapper.completer(); var c = PromiseWrapper.completer();
_zone.run(() => { _zone.run(() => {
@ -86,7 +87,7 @@ export function main() {
it('should produce long stack traces (when using microtasks)', it('should produce long stack traces (when using microtasks)',
inject([AsyncTestCompleter], (async) => { inject([AsyncTestCompleter], (async) => {
macroTask(() => { macroTask(() => {
_zone.initCallbacks({onErrorHandler: logError}); _zone.overrideOnErrorHandler(logError);
var c = PromiseWrapper.completer(); var c = PromiseWrapper.completer();
_zone.run(() => { _zone.run(() => {
@ -114,7 +115,7 @@ export function main() {
it('should disable long stack traces', inject([AsyncTestCompleter], (async) => { it('should disable long stack traces', inject([AsyncTestCompleter], (async) => {
macroTask(() => { macroTask(() => {
_zone.initCallbacks({onErrorHandler: logError}); _zone.overrideOnErrorHandler(logError);
var c = PromiseWrapper.completer(); var c = PromiseWrapper.completer();
_zone.run(() => { _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', it('should call onTurnStart once before a turn and onTurnDone once after the turn',
inject([AsyncTestCompleter], (async) => { inject([AsyncTestCompleter], (async) => {
@ -201,12 +273,11 @@ function commonTests() {
it('should not run onTurnStart and onTurnDone for nested Zone.run invoked from onTurnDone', it('should not run onTurnStart and onTurnDone for nested Zone.run invoked from onTurnDone',
inject([AsyncTestCompleter], (async) => { inject([AsyncTestCompleter], (async) => {
_zone.initCallbacks({ _zone.overrideOnTurnStart(null);
onTurnDone: () => { _zone.overrideOnTurnDone(() => {
_log.add('onTurnDone:started'); _log.add('onTurnDone:started');
_zone.run(() => _log.add('nested run')); _zone.run(() => _log.add('nested run'));
_log.add('onTurnDone:finished'); _log.add('onTurnDone:finished');
}
}); });
macroTask(() => { _zone.run(() => { _log.add('start run'); }); }); macroTask(() => { _zone.run(() => { _log.add('start run'); }); });
@ -306,19 +377,17 @@ function commonTests() {
'onTurnDone after executing the task', 'onTurnDone after executing the task',
inject([AsyncTestCompleter], (async) => { inject([AsyncTestCompleter], (async) => {
var ran = false; var ran = false;
_zone.initCallbacks({ _zone.overrideOnTurnStart(_log.fn('onTurnStart'));
onTurnStart: _log.fn('onTurnStart'), _zone.overrideOnTurnDone(() => {
onTurnDone: () => { _log.add('onTurnDone(begin)');
_log.add('onTurnDone(begin)'); if (!ran) {
if (!ran) { microTask(() => {
microTask(() => { ran = true;
ran = true; _log.add('executedMicrotask');
_log.add('executedMicrotask'); });
});
}
_log.add('onTurnDone(end)');
} }
_log.add('onTurnDone(end)');
}); });
macroTask(() => { _zone.run(_log.fn('run')); }); macroTask(() => { _zone.run(_log.fn('run')); });
@ -338,19 +407,17 @@ function commonTests() {
'a scheduleMicrotask in run', 'a scheduleMicrotask in run',
inject([AsyncTestCompleter], (async) => { inject([AsyncTestCompleter], (async) => {
var ran = false; var ran = false;
_zone.initCallbacks({ _zone.overrideOnTurnStart(_log.fn('onTurnStart'));
onTurnStart: _log.fn('onTurnStart'), _zone.overrideOnTurnDone(() => {
onTurnDone: () => { _log.add('onTurnDone(begin)');
_log.add('onTurnDone(begin)'); if (!ran) {
if (!ran) { _log.add('onTurnDone(scheduleMicrotask)');
_log.add('onTurnDone(scheduleMicrotask)'); microTask(() => {
microTask(() => { ran = true;
ran = true; _log.add('onTurnDone(executeMicrotask)');
_log.add('onTurnDone(executeMicrotask)'); });
});
}
_log.add('onTurnDone(end)');
} }
_log.add('onTurnDone(end)');
}); });
macroTask(() => { macroTask(() => {
@ -376,25 +443,23 @@ function commonTests() {
var donePromiseRan = false; var donePromiseRan = false;
var startPromiseRan = false; var startPromiseRan = false;
_zone.initCallbacks({ _zone.overrideOnTurnStart(() => {
onTurnStart: () => { _log.add('onTurnStart(begin)');
_log.add('onTurnStart(begin)'); if (!startPromiseRan) {
if (!startPromiseRan) { _log.add('onTurnStart(schedulePromise)');
_log.add('onTurnStart(schedulePromise)'); microTask(_log.fn('onTurnStart(executePromise)'));
microTask(_log.fn('onTurnStart(executePromise)')); startPromiseRan = true;
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)');
} }
_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(() => { macroTask(() => {
@ -507,7 +572,7 @@ function commonTests() {
it('should call the on error callback when it is defined', it('should call the on error callback when it is defined',
inject([AsyncTestCompleter], (async) => { inject([AsyncTestCompleter], (async) => {
macroTask(() => { macroTask(() => {
_zone.initCallbacks({onErrorHandler: logError}); _zone.overrideOnErrorHandler(logError);
var exception = new BaseException('sync'); var exception = new BaseException('sync');
@ -520,7 +585,7 @@ function commonTests() {
})); }));
it('should call onError for errors from microtasks', inject([AsyncTestCompleter], (async) => { it('should call onError for errors from microtasks', inject([AsyncTestCompleter], (async) => {
_zone.initCallbacks({onErrorHandler: logError}); _zone.overrideOnErrorHandler(logError);
var exception = new BaseException('async'); var exception = new BaseException('async');
@ -537,7 +602,8 @@ function commonTests() {
inject([AsyncTestCompleter], (async) => { inject([AsyncTestCompleter], (async) => {
var exception = new BaseException('fromOnTurnDone'); var exception = new BaseException('fromOnTurnDone');
_zone.initCallbacks({onErrorHandler: logError, onTurnDone: () => { throw exception; }}); _zone.overrideOnErrorHandler(logError);
_zone.overrideOnTurnDone(() => { throw exception; });
macroTask(() => { _zone.run(() => {}); }); macroTask(() => { _zone.run(() => {}); });
@ -554,7 +620,8 @@ function commonTests() {
var exception = new BaseException('fromOnTurnDone'); 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; }); }); }); macroTask(() => { _zone.run(() => { microTask(() => { asyncRan = true; }); }); });