fix(core): don’t stop change detection because of errors

- prevents unsubscribing from the zone on error
- prevents unsubscribing from directive `EventEmitter`s on error
- prevents detaching views in dev mode if there on error
- ensures that `ngOnInit` is only called 1x (also in prod mode)

Fixes #9531
Fixes #2413
Fixes #15925
This commit is contained in:
Tobias Bosch
2017-04-28 11:50:45 -07:00
committed by Matias Niemelä
parent ac220fc2bb
commit e263e19a2a
16 changed files with 218 additions and 68 deletions

View File

@ -560,6 +560,9 @@ export class ApplicationRef_ extends ApplicationRef {
if (this._enforceNoNewChanges) {
this._views.forEach((view) => view.checkNoChanges());
}
} catch (e) {
// Attention: Don't rethrow as it could cancel subscriptions to Observables!
this._exceptionHandler.handleError(e);
} finally {
this._runningTick = false;
wtfLeave(scope);

View File

@ -189,7 +189,14 @@ export function listenToElementOutputs(view: ViewData, compView: ViewData, def:
}
function renderEventHandlerClosure(view: ViewData, index: number, eventName: string) {
return (event: any) => dispatchEvent(view, index, eventName, event);
return (event: any) => {
try {
return dispatchEvent(view, index, eventName, event);
} catch (e) {
// Attention: Don't rethrow, to keep in sync with directive events.
view.root.errorHandler.handleError(e);
}
}
}

View File

@ -148,7 +148,14 @@ export function createDirectiveInstance(view: ViewData, def: NodeDef): any {
}
function eventHandlerClosure(view: ViewData, index: number, eventName: string) {
return (event: any) => dispatchEvent(view, index, eventName, event);
return (event: any) => {
try {
return dispatchEvent(view, index, eventName, event);
} catch (e) {
// Attention: Don't rethrow, as it would cancel Observable subscriptions!
view.root.errorHandler.handleError(e);
}
}
}
export function checkAndUpdateDirectiveInline(

View File

@ -9,6 +9,7 @@
import {isDevMode} from '../application_ref';
import {DebugElement, DebugNode, EventListener, getDebugNode, indexDebugNode, removeDebugNodeFromIndex} from '../debug/debug_node';
import {Injector} from '../di';
import {ErrorHandler} from '../error_handler';
import {NgModuleRef} from '../linker/ng_module_factory';
import {Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2} from '../render/api';
import {Sanitizer} from '../security';
@ -104,11 +105,12 @@ function createRootData(
elInjector: Injector, ngModule: NgModuleRef<any>, rendererFactory: RendererFactory2,
projectableNodes: any[][], rootSelectorOrNode: any): RootData {
const sanitizer = ngModule.injector.get(Sanitizer);
const errorHandler = ngModule.injector.get(ErrorHandler);
const renderer = rendererFactory.createRenderer(null, null);
return {
ngModule,
injector: elInjector, projectableNodes,
selectorOrNode: rootSelectorOrNode, sanitizer, rendererFactory, renderer
selectorOrNode: rootSelectorOrNode, sanitizer, rendererFactory, renderer, errorHandler
};
}
@ -439,7 +441,6 @@ function callWithDebugContext(action: DebugAction, fn: any, self: any, args: any
if (isViewDebugError(e) || !_currentView) {
throw e;
}
_currentView.state |= ViewState.Errored;
throw viewWrappedDebugError(e, getCurrentDebugContext() !);
}
}

View File

@ -7,6 +7,7 @@
*/
import {Injector} from '../di';
import {ErrorHandler} from '../error_handler';
import {NgModuleRef} from '../linker/ng_module_factory';
import {QueryList} from '../linker/query_list';
import {TemplateRef} from '../linker/template_ref';
@ -323,13 +324,14 @@ export interface ViewData {
* Bitmask of states
*/
export const enum ViewState {
FirstCheck = 1 << 0,
Attached = 1 << 1,
ChecksEnabled = 1 << 2,
Errored = 1 << 3,
BeforeFirstCheck = 1 << 0,
FirstCheck = 1 << 1,
Attached = 1 << 2,
ChecksEnabled = 1 << 3,
Destroyed = 1 << 4,
CatDetectChanges = Attached | ChecksEnabled,
CatInit = BeforeFirstCheck | CatDetectChanges
}
export interface DisposableFn { (): void; }
@ -432,6 +434,7 @@ export interface RootData {
selectorOrNode: any;
renderer: Renderer2;
rendererFactory: RendererFactory2;
errorHandler: ErrorHandler;
sanitizer: Sanitizer;
}

View File

@ -100,10 +100,10 @@ export function checkAndUpdateBinding(
export function checkBindingNoChanges(
view: ViewData, def: NodeDef, bindingIdx: number, value: any) {
const oldValue = view.oldValues[def.bindingIndex + bindingIdx];
if ((view.state & ViewState.FirstCheck) || !devModeEqual(oldValue, value)) {
if ((view.state & ViewState.BeforeFirstCheck) || !devModeEqual(oldValue, value)) {
throw expressionChangedAfterItHasBeenCheckedError(
Services.createDebugContext(view, def.index), oldValue, value,
(view.state & ViewState.FirstCheck) !== 0);
(view.state & ViewState.BeforeFirstCheck) !== 0);
}
}

View File

@ -211,7 +211,7 @@ function createView(
viewContainerParent: null, parentNodeDef,
context: null,
component: null, nodes,
state: ViewState.FirstCheck | ViewState.CatDetectChanges, root, renderer,
state: ViewState.CatInit, root, renderer,
oldValues: new Array(def.bindingCount), disposables
};
return view;
@ -323,6 +323,12 @@ export function checkNoChangesView(view: ViewData) {
}
export function checkAndUpdateView(view: ViewData) {
if (view.state & ViewState.BeforeFirstCheck) {
view.state &= ~ViewState.BeforeFirstCheck;
view.state |= ViewState.FirstCheck;
} else {
view.state &= ~ViewState.FirstCheck;
}
Services.updateDirectives(view, CheckType.CheckAndUpdate);
execEmbeddedViewsAction(view, ViewAction.CheckAndUpdate);
execQueriesAction(
@ -345,7 +351,6 @@ export function checkAndUpdateView(view: ViewData) {
if (view.def.flags & ViewFlags.OnPush) {
view.state &= ~ViewState.ChecksEnabled;
}
view.state &= ~ViewState.FirstCheck;
}
export function checkAndUpdateNode(
@ -453,7 +458,7 @@ function checkNoChangesQuery(view: ViewData, nodeDef: NodeDef) {
if (queryList.dirty) {
throw expressionChangedAfterItHasBeenCheckedError(
Services.createDebugContext(view, nodeDef.index), `Query ${nodeDef.query!.id} not dirty`,
`Query ${nodeDef.query!.id} dirty`, (view.state & ViewState.FirstCheck) !== 0);
`Query ${nodeDef.query!.id} dirty`, (view.state & ViewState.BeforeFirstCheck) !== 0);
}
}
@ -543,13 +548,13 @@ function callViewAction(view: ViewData, action: ViewAction) {
switch (action) {
case ViewAction.CheckNoChanges:
if ((viewState & ViewState.CatDetectChanges) === ViewState.CatDetectChanges &&
(viewState & (ViewState.Errored | ViewState.Destroyed)) === 0) {
(viewState & ViewState.Destroyed) === 0) {
checkNoChangesView(view);
}
break;
case ViewAction.CheckAndUpdate:
if ((viewState & ViewState.CatDetectChanges) === ViewState.CatDetectChanges &&
(viewState & (ViewState.Errored | ViewState.Destroyed)) === 0) {
(viewState & ViewState.Destroyed) === 0) {
checkAndUpdateView(view);
}
break;