fix(ivy): prevent errors from views being destroyed twice (#28413)

Previously, attempting to destroy a view with listeners more than once
throws an error during event listener cleanup. This happens because
`cleanup` field on the `TView` has already been cleared out by the time
the second destruction runs.

The `destroyed` flag on LView was previously being set in the `destroyLView` function,
but this flag was never _checked_ anywhere in the codebase. This commit
moves _setting_ this flag to the `cleanupView` function, just before
destroy hooks are called. This is necessary because the destroy hooks
can contain arbitrary user code, such as (surprise!) attempting to
destroy the view (again). We also add a check to `destroyLView` to skip
already-destroyed views. This prevents the cleanup code path from running twice.

PR Close #28413
This commit is contained in:
Jeremy Elbourn
2019-01-28 15:23:53 -08:00
committed by Matias Niemelä
parent b35ef184a7
commit 35e45dc894
3 changed files with 79 additions and 12 deletions

View File

@ -374,13 +374,14 @@ export function getLViewChild(lView: LView): LView|LContainer|null {
* @param view The view to be destroyed.
*/
export function destroyLView(view: LView) {
const renderer = view[RENDERER];
if (isProceduralRenderer(renderer) && renderer.destroyNode) {
walkTNodeTree(view, WalkTNodeTreeAction.Destroy, renderer, null);
if (!(view[FLAGS] & LViewFlags.Destroyed)) {
const renderer = view[RENDERER];
if (isProceduralRenderer(renderer) && renderer.destroyNode) {
walkTNodeTree(view, WalkTNodeTreeAction.Destroy, renderer, null);
}
destroyViewTree(view);
}
destroyViewTree(view);
// Sets the destroyed flag
view[FLAGS] |= LViewFlags.Destroyed;
}
/**
@ -418,6 +419,14 @@ export function getParentState(state: LView | LContainer, rootView: LView): LVie
function cleanUpView(viewOrContainer: LView | LContainer): void {
if ((viewOrContainer as LView).length >= HEADER_OFFSET) {
const view = viewOrContainer as LView;
// Mark the LView as destroyed *before* executing the onDestroy hooks. An onDestroy hook
// runs arbitrary user code, which could include its own `viewRef.destroy()` (or similar). If
// We don't flag the view as destroyed before the hooks, this could lead to an infinite loop.
// This also aligns with the ViewEngine behavior. It also means that the onDestroy hook is
// really more of an "afterDestroy" hook if you think about it.
view[FLAGS] |= LViewFlags.Destroyed;
executeOnDestroys(view);
removeListeners(view);
const hostTNode = view[HOST_NODE];