angular2
Version:
Angular 2 - a web framework for modern web apps
455 lines (419 loc) • 15.5 kB
text/typescript
import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {normalizeBlank, isPresent, global} from 'angular2/src/facade/lang';
import {ObservableWrapper, EventEmitter} from 'angular2/src/facade/async';
import {wtfLeave, wtfCreateScope, WtfScopeFn} from '../profile/profile';
export interface NgZoneZone extends Zone {
/** @internal */
_innerZone: boolean;
}
export interface ZeroArgFunction { (): void; }
export interface ErrorHandlingFn { (error: any, stackTrace: any): void; }
/**
* Stores error information; delivered via [NgZone.onError] stream.
*/
export class NgZoneError {
constructor(public error: any, public stackTrace: any) {}
}
/**
* An injectable service for executing work inside or outside of the Angular zone.
*
* The most common use of this service is to optimize performance when starting a work consisting of
* one or more asynchronous tasks that don't require UI updates or error handling to be handled by
* Angular. Such tasks can be kicked off via {@link #runOutsideAngular} and if needed, these tasks
* can reenter the Angular zone via {@link #run}.
*
* <!-- TODO: add/fix links to:
* - docs explaining zones and the use of zones in Angular and change-detection
* - link to runOutsideAngular/run (throughout this file!)
* -->
*
* ### Example ([live demo](http://plnkr.co/edit/lY9m8HLy7z06vDoUaSN2?p=preview))
* ```
* import {Component, View, NgIf, NgZone} from 'angular2/angular2';
*
* @Component({
* selector: 'ng-zone-demo'.
* template: `
* <h2>Demo: NgZone</h2>
*
* <p>Progress: {{progress}}%</p>
* <p *ng-if="progress >= 100">Done processing {{label}} of Angular zone!</p>
*
* <button (click)="processWithinAngularZone()">Process within Angular zone</button>
* <button (click)="processOutsideOfAngularZone()">Process outside of Angular zone</button>
* `,
* directives: [NgIf]
* })
* export class NgZoneDemo {
* progress: number = 0;
* label: string;
*
* constructor(private _ngZone: NgZone) {}
*
* // Loop inside the Angular zone
* // so the UI DOES refresh after each setTimeout cycle
* processWithinAngularZone() {
* this.label = 'inside';
* this.progress = 0;
* this._increaseProgress(() => console.log('Inside Done!'));
* }
*
* // Loop outside of the Angular zone
* // so the UI DOES NOT refresh after each setTimeout cycle
* processOutsideOfAngularZone() {
* this.label = 'outside';
* this.progress = 0;
* this._ngZone.runOutsideAngular(() => {
* this._increaseProgress(() => {
* // reenter the Angular zone and display done
* this._ngZone.run(() => {console.log('Outside Done!') });
* }}));
* }
*
*
* _increaseProgress(doneCallback: () => void) {
* this.progress += 1;
* console.log(`Current progress: ${this.progress}%`);
*
* if (this.progress < 100) {
* window.setTimeout(() => this._increaseProgress(doneCallback)), 10)
* } else {
* doneCallback();
* }
* }
* }
* ```
*/
export class NgZone {
/** @internal */
_runScope: WtfScopeFn = wtfCreateScope(`NgZone#run()`);
/** @internal */
_microtaskScope: WtfScopeFn = wtfCreateScope(`NgZone#microtask()`);
// Code executed in _mountZone does not trigger the onTurnDone.
/** @internal */
_mountZone;
// _innerZone is the child of _mountZone. Any code executed in this zone will trigger the
// onTurnDone hook at the end of the current VM turn.
/** @internal */
_innerZone;
/** @internal */
_onTurnStart: ZeroArgFunction;
/** @internal */
_onTurnDone: ZeroArgFunction;
/** @internal */
_onEventDone: ZeroArgFunction;
/** @internal */
_onErrorHandler: ErrorHandlingFn;
/** @internal */
_onTurnStartEvents: EventEmitter<any>;
/** @internal */
_onTurnDoneEvents: EventEmitter<any>;
/** @internal */
_onEventDoneEvents: EventEmitter<any>;
/** @internal */
_onErrorEvents: EventEmitter<any>;
// Number of microtasks pending from _innerZone (& descendants)
/** @internal */
_pendingMicrotasks: number = 0;
// Whether some code has been executed in the _innerZone (& descendants) in the current turn
/** @internal */
_hasExecutedCodeInInnerZone: boolean = false;
// run() call depth in _mountZone. 0 at the end of a macrotask
// zone.run(() => { // top-level call
// zone.run(() => {}); // nested call -> in-turn
// });
/** @internal */
_nestedRun: number = 0;
// TODO(vicb): implement this class properly for node.js environment
// This disabled flag is only here to please cjs tests
/** @internal */
_disabled: boolean;
/** @internal */
_inVmTurnDone: boolean = false;
/** @internal */
_pendingTimeouts: number[] = [];
/**
* @param {bool} enableLongStackTrace whether to enable long stack trace. They should only be
* enabled in development mode as they significantly impact perf.
*/
constructor({enableLongStackTrace}) {
if (global.zone) {
this._disabled = false;
this._mountZone = global.zone;
this._innerZone = this._createInnerZone(this._mountZone, enableLongStackTrace);
} else {
this._disabled = true;
this._mountZone = null;
}
this._onTurnStartEvents = new EventEmitter(false);
this._onTurnDoneEvents = new EventEmitter(false);
this._onEventDoneEvents = new EventEmitter(false);
this._onErrorEvents = new EventEmitter(false);
}
/**
* Sets the zone hook that is called just before a browser task that is handled by Angular
* executes.
*
* The hook is called once per browser task that is handled by Angular.
*
* Setting the hook overrides any previously set hook.
*
* @deprecated this API will be removed in the future. Use `onTurnStart` instead.
*/
overrideOnTurnStart(onTurnStartHook: ZeroArgFunction): void {
this._onTurnStart = normalizeBlank(onTurnStartHook);
}
/**
* Notifies subscribers just before Angular event turn starts.
*
* Emits an event once per browser task that is handled by Angular.
*/
get onTurnStart(): /* Subject */ any { return this._onTurnStartEvents; }
/** @internal */
_notifyOnTurnStart(parentRun): void {
parentRun.call(this._innerZone, () => { this._onTurnStartEvents.emit(null); });
}
/**
* Sets the zone hook that is called immediately after Angular zone is done processing the current
* task and any microtasks scheduled from that task.
*
* This is where we typically do change-detection.
*
* The hook is called once per browser task that is handled by Angular.
*
* Setting the hook overrides any previously set hook.
*
* @deprecated this API will be removed in the future. Use `onTurnDone` instead.
*/
overrideOnTurnDone(onTurnDoneHook: ZeroArgFunction): void {
this._onTurnDone = normalizeBlank(onTurnDoneHook);
}
/**
* Notifies subscribers immediately after Angular zone is done processing
* the current turn and any microtasks scheduled from that turn.
*
* Used by Angular as a signal to kick off change-detection.
*/
get onTurnDone() { return this._onTurnDoneEvents; }
/** @internal */
_notifyOnTurnDone(parentRun): void {
parentRun.call(this._innerZone, () => { this._onTurnDoneEvents.emit(null); });
}
/**
* Sets the zone hook that is called immediately after the `onTurnDone` callback is called and any
* microstasks scheduled from within that callback are drained.
*
* `onEventDoneFn` is executed outside Angular zone, which means that we will no longer attempt to
* sync the UI with any model changes that occur within this callback.
*
* This hook is useful for validating application state (e.g. in a test).
*
* Setting the hook overrides any previously set hook.
*
* @deprecated this API will be removed in the future. Use `onEventDone` instead.
*/
overrideOnEventDone(onEventDoneFn: ZeroArgFunction, opt_waitForAsync: boolean = false): void {
var normalizedOnEventDone = normalizeBlank(onEventDoneFn);
if (opt_waitForAsync) {
this._onEventDone = () => {
if (!this._pendingTimeouts.length) {
normalizedOnEventDone();
}
};
} else {
this._onEventDone = normalizedOnEventDone;
}
}
/**
* Notifies subscribers immediately after the final `onTurnDone` callback
* before ending VM event.
*
* This event is useful for validating application state (e.g. in a test).
*/
get onEventDone() { return this._onEventDoneEvents; }
/** @internal */
_notifyOnEventDone(): void {
this.runOutsideAngular(() => { this._onEventDoneEvents.emit(null); });
}
/**
* Whether there are any outstanding microtasks.
*/
get hasPendingMicrotasks(): boolean { return this._pendingMicrotasks > 0; }
/**
* Whether there are any outstanding timers.
*/
get hasPendingTimers(): boolean { return this._pendingTimeouts.length > 0; }
/**
* Whether there are any outstanding asychnronous tasks of any kind that are
* scheduled to run within Angular zone.
*
* Useful as a signal of UI stability. For example, when a test reaches a
* point when [hasPendingAsyncTasks] is `false` it might be a good time to run
* test expectations.
*/
get hasPendingAsyncTasks(): boolean { return this.hasPendingMicrotasks || this.hasPendingTimers; }
/**
* Sets the zone hook that is called when an error is thrown in the Angular zone.
*
* Setting the hook overrides any previously set hook.
*
* @deprecated this API will be removed in the future. Use `onError` instead.
*/
overrideOnErrorHandler(errorHandler: ErrorHandlingFn) {
this._onErrorHandler = normalizeBlank(errorHandler);
}
get onError() { return this._onErrorEvents; }
/**
* Executes the `fn` function synchronously within the Angular zone and returns value returned by
* the function.
*
* Running functions via `run` allows you to reenter Angular zone from a task that was executed
* outside of the Angular zone (typically started via {@link #runOutsideAngular}).
*
* Any future tasks or microtasks scheduled from within this function will continue executing from
* within the Angular zone.
*/
run(fn: () => any): any {
if (this._disabled) {
return fn();
} else {
var s = this._runScope();
try {
return this._innerZone.run(fn);
} finally {
wtfLeave(s);
}
}
}
/**
* Executes the `fn` function synchronously in Angular's parent zone and returns value returned by
* the function.
*
* Running functions via `runOutsideAngular` allows you to escape Angular's zone and do work that
* doesn't trigger Angular change-detection or is subject to Angular's error handling.
*
* Any future tasks or microtasks scheduled from within this function will continue executing from
* outside of the Angular zone.
*
* Use {@link #run} to reenter the Angular zone and do work that updates the application model.
*/
runOutsideAngular(fn: () => any): any {
if (this._disabled) {
return fn();
} else {
return this._mountZone.run(fn);
}
}
/** @internal */
_createInnerZone(zone, enableLongStackTrace) {
var microtaskScope = this._microtaskScope;
var ngZone = this;
var errorHandling;
if (enableLongStackTrace) {
errorHandling = StringMapWrapper.merge(
Zone.longStackTraceZone, {onError: function(e) { ngZone._notifyOnError(this, e); }});
} else {
errorHandling = {onError: function(e) { ngZone._notifyOnError(this, e); }};
}
return zone.fork(errorHandling)
.fork({
'$run': function(parentRun) {
return function() {
try {
ngZone._nestedRun++;
if (!ngZone._hasExecutedCodeInInnerZone) {
ngZone._hasExecutedCodeInInnerZone = true;
ngZone._notifyOnTurnStart(parentRun);
if (ngZone._onTurnStart) {
parentRun.call(ngZone._innerZone, ngZone._onTurnStart);
}
}
return parentRun.apply(this, arguments);
} finally {
ngZone._nestedRun--;
// If there are no more pending microtasks, we are at the end of a VM turn (or in
// onTurnStart)
// _nestedRun will be 0 at the end of a macrotasks (it could be > 0 when there are
// nested calls
// to run()).
if (ngZone._pendingMicrotasks == 0 && ngZone._nestedRun == 0 &&
!this._inVmTurnDone) {
if (ngZone._hasExecutedCodeInInnerZone) {
try {
this._inVmTurnDone = true;
ngZone._notifyOnTurnDone(parentRun);
if (ngZone._onTurnDone) {
parentRun.call(ngZone._innerZone, ngZone._onTurnDone);
}
} finally {
this._inVmTurnDone = false;
ngZone._hasExecutedCodeInInnerZone = false;
}
}
if (ngZone._pendingMicrotasks === 0) {
ngZone._notifyOnEventDone();
if (isPresent(ngZone._onEventDone)) {
ngZone.runOutsideAngular(ngZone._onEventDone);
}
}
}
}
};
},
'$scheduleMicrotask': function(parentScheduleMicrotask) {
return function(fn) {
ngZone._pendingMicrotasks++;
var microtask = function() {
var s = microtaskScope();
try {
fn();
} finally {
ngZone._pendingMicrotasks--;
wtfLeave(s);
}
};
parentScheduleMicrotask.call(this, microtask);
};
},
'$setTimeout': function(parentSetTimeout) {
return function(fn: Function, delay: number, ...args) {
var id;
var cb = function() {
fn();
ListWrapper.remove(ngZone._pendingTimeouts, id);
};
id = parentSetTimeout(cb, delay, args);
ngZone._pendingTimeouts.push(id);
return id;
};
},
'$clearTimeout': function(parentClearTimeout) {
return function(id: number) {
parentClearTimeout(id);
ListWrapper.remove(ngZone._pendingTimeouts, id);
};
},
_innerZone: true
});
}
/** @internal */
_notifyOnError(zone, e): void {
if (isPresent(this._onErrorHandler) || ObservableWrapper.hasSubscribers(this._onErrorEvents)) {
var trace = [normalizeBlank(e.stack)];
while (zone && zone.constructedAtException) {
trace.push(zone.constructedAtException.get());
zone = zone.parent;
}
if (ObservableWrapper.hasSubscribers(this._onErrorEvents)) {
ObservableWrapper.callEmit(this._onErrorEvents, new NgZoneError(e, trace));
}
if (isPresent(this._onErrorHandler)) {
this._onErrorHandler(e, trace);
}
} else {
console.log('## _notifyOnError ##');
console.log(e.stack);
throw e;
}
}
}