fix(core): remove application from the testability registry when the root view is removed (#39876)
In the new behavior Angular removes applications from the testability registry when the root view gets destroyed. This eliminates a memory leak, because before that the TestabilityRegistry holds references to HTML elements, thus they cannot be GCed. PR Close #22106 PR Close #39876
This commit is contained in:
parent
75fc89384d
commit
df27027ecb
|
@ -39,7 +39,7 @@
|
|||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 2285,
|
||||
"main-es2015": 241879,
|
||||
"main-es2015": 242455,
|
||||
"polyfills-es2015": 36709,
|
||||
"5-es2015": 745
|
||||
}
|
||||
|
|
|
@ -594,6 +594,7 @@ export class ApplicationRef {
|
|||
private _runningTick: boolean = false;
|
||||
private _enforceNoNewChanges: boolean = false;
|
||||
private _stable = true;
|
||||
private _onMicrotaskEmptySubscription: Subscription;
|
||||
|
||||
/**
|
||||
* Get a list of component types registered to this application.
|
||||
|
@ -622,7 +623,7 @@ export class ApplicationRef {
|
|||
private _initStatus: ApplicationInitStatus) {
|
||||
this._enforceNoNewChanges = isDevMode();
|
||||
|
||||
this._zone.onMicrotaskEmpty.subscribe({
|
||||
this._onMicrotaskEmptySubscription = this._zone.onMicrotaskEmpty.subscribe({
|
||||
next: () => {
|
||||
this._zone.run(() => {
|
||||
this.tick();
|
||||
|
@ -715,15 +716,20 @@ export class ApplicationRef {
|
|||
isBoundToModule(componentFactory) ? undefined : this._injector.get(NgModuleRef);
|
||||
const selectorOrNode = rootSelectorOrNode || componentFactory.selector;
|
||||
const compRef = componentFactory.create(Injector.NULL, [], selectorOrNode, ngModule);
|
||||
const nativeElement = compRef.location.nativeElement;
|
||||
const testability = compRef.injector.get(Testability, null);
|
||||
const testabilityRegistry = testability && compRef.injector.get(TestabilityRegistry);
|
||||
if (testability && testabilityRegistry) {
|
||||
testabilityRegistry.registerApplication(nativeElement, testability);
|
||||
}
|
||||
|
||||
compRef.onDestroy(() => {
|
||||
this._unloadComponent(compRef);
|
||||
});
|
||||
const testability = compRef.injector.get(Testability, null);
|
||||
if (testability) {
|
||||
compRef.injector.get(TestabilityRegistry)
|
||||
.registerApplication(compRef.location.nativeElement, testability);
|
||||
this.detachView(compRef.hostView);
|
||||
remove(this.components, compRef);
|
||||
if (testabilityRegistry) {
|
||||
testabilityRegistry.unregisterApplication(nativeElement);
|
||||
}
|
||||
});
|
||||
|
||||
this._loadComponent(compRef);
|
||||
if (isDevMode()) {
|
||||
|
@ -796,15 +802,11 @@ export class ApplicationRef {
|
|||
listeners.forEach((listener) => listener(componentRef));
|
||||
}
|
||||
|
||||
private _unloadComponent(componentRef: ComponentRef<any>): void {
|
||||
this.detachView(componentRef.hostView);
|
||||
remove(this.components, componentRef);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
ngOnDestroy() {
|
||||
// TODO(alxhub): Dispose of the NgZone.
|
||||
this._views.slice().forEach((view) => view.destroy());
|
||||
this._onMicrotaskEmptySubscription.unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -248,7 +248,6 @@ export function injectComponentFactoryResolver(): viewEngine_ComponentFactoryRes
|
|||
*
|
||||
*/
|
||||
export class ComponentRef<T> extends viewEngine_ComponentRef<T> {
|
||||
destroyCbs: (() => void)[]|null = [];
|
||||
instance: T;
|
||||
hostView: ViewRef<T>;
|
||||
changeDetectorRef: ViewEngine_ChangeDetectorRef;
|
||||
|
@ -269,16 +268,10 @@ export class ComponentRef<T> extends viewEngine_ComponentRef<T> {
|
|||
}
|
||||
|
||||
destroy(): void {
|
||||
if (this.destroyCbs) {
|
||||
this.destroyCbs.forEach(fn => fn());
|
||||
this.destroyCbs = null;
|
||||
!this.hostView.destroyed && this.hostView.destroy();
|
||||
}
|
||||
this.hostView.destroy();
|
||||
}
|
||||
|
||||
onDestroy(callback: () => void): void {
|
||||
if (this.destroyCbs) {
|
||||
this.destroyCbs.push(callback);
|
||||
}
|
||||
this.hostView.onDestroy(callback);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import {assertTNodeType} from '../node_assert';
|
|||
import {getCurrentDirectiveDef, getCurrentTNode, getLView, getTView} from '../state';
|
||||
import {getComponentLViewByIndex, getNativeByTNode, unwrapRNode} from '../util/view_utils';
|
||||
|
||||
import {getLCleanup, handleError, loadComponentRenderer, markViewDirty} from './shared';
|
||||
import {getLCleanup, getTViewCleanup, handleError, loadComponentRenderer, markViewDirty} from './shared';
|
||||
|
||||
|
||||
|
||||
|
@ -120,7 +120,7 @@ function listenerInternal(
|
|||
eventTargetResolver?: GlobalTargetResolver): void {
|
||||
const isTNodeDirectiveHost = isDirectiveHost(tNode);
|
||||
const firstCreatePass = tView.firstCreatePass;
|
||||
const tCleanup: false|any[] = firstCreatePass && (tView.cleanup || (tView.cleanup = []));
|
||||
const tCleanup: false|any[] = firstCreatePass && getTViewCleanup(tView);
|
||||
|
||||
// When the ɵɵlistener instruction was generated and is executed we know that there is either a
|
||||
// native listener or a directive output on this element. As such we we know that we will have to
|
||||
|
|
|
@ -752,16 +752,28 @@ export function locateHostElement(
|
|||
* On the first template pass, saves in TView:
|
||||
* - Cleanup function
|
||||
* - Index of context we just saved in LView.cleanupInstances
|
||||
*
|
||||
* This function can also be used to store instance specific cleanup fns. In that case the `context`
|
||||
* is `null` and the function is store in `LView` (rather than it `TView`).
|
||||
*/
|
||||
export function storeCleanupWithContext(
|
||||
tView: TView, lView: LView, context: any, cleanupFn: Function): void {
|
||||
const lCleanup = getLCleanup(lView);
|
||||
if (context === null) {
|
||||
// If context is null that this is instance specific callback. These callbacks can only be
|
||||
// inserted after template shared instances. For this reason in ngDevMode we freeze the TView.
|
||||
if (ngDevMode) {
|
||||
Object.freeze(getTViewCleanup(tView));
|
||||
}
|
||||
lCleanup.push(cleanupFn);
|
||||
} else {
|
||||
lCleanup.push(context);
|
||||
|
||||
if (tView.firstCreatePass) {
|
||||
getTViewCleanup(tView).push(cleanupFn, lCleanup.length - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a TNode object from the arguments.
|
||||
|
@ -1997,7 +2009,7 @@ export function getLCleanup(view: LView): any[] {
|
|||
return view[CLEANUP] || (view[CLEANUP] = ngDevMode ? new LCleanup() : []);
|
||||
}
|
||||
|
||||
function getTViewCleanup(tView: TView): any[] {
|
||||
export function getTViewCleanup(tView: TView): any[] {
|
||||
return tView.cleanup || (tView.cleanup = ngDevMode ? new TCleanup() : []);
|
||||
}
|
||||
|
||||
|
|
|
@ -163,8 +163,11 @@ export interface LView extends Array<any> {
|
|||
*
|
||||
* These change per LView instance, so they cannot be stored on TView. Instead,
|
||||
* TView.cleanup saves an index to the necessary context in this array.
|
||||
*
|
||||
* After `LView` is created it is possible to attach additional instance specific functions at the
|
||||
* end of the `lView[CLENUP]` because we know that no more `T` level cleanup functions will be
|
||||
* addeded here.
|
||||
*/
|
||||
// TODO: flatten into LView[]
|
||||
[CLEANUP]: any[]|null;
|
||||
|
||||
/**
|
||||
|
|
|
@ -10,7 +10,7 @@ import {ViewEncapsulation} from '../metadata/view';
|
|||
import {Renderer2} from '../render/api';
|
||||
import {RendererStyleFlags2} from '../render/api_flags';
|
||||
import {addToArray, removeFromArray} from '../util/array_utils';
|
||||
import {assertDefined, assertDomNode, assertEqual, assertString} from '../util/assert';
|
||||
import {assertDefined, assertDomNode, assertEqual, assertFunction, assertString} from '../util/assert';
|
||||
import {assertLContainer, assertLView, assertTNodeForLView} from './assert';
|
||||
import {attachPatchData} from './context_discovery';
|
||||
import {icuContainerIterate} from './i18n/i18n_tree_shaking';
|
||||
|
@ -418,7 +418,7 @@ function cleanUpView(tView: TView, lView: LView): void {
|
|||
lView[FLAGS] |= LViewFlags.Destroyed;
|
||||
|
||||
executeOnDestroys(tView, lView);
|
||||
removeListeners(tView, lView);
|
||||
processCleanups(tView, lView);
|
||||
// For component views only, the local renderer is destroyed at clean up time.
|
||||
if (lView[TVIEW].type === TViewType.Component && isProceduralRenderer(lView[RENDERER])) {
|
||||
ngDevMode && ngDevMode.rendererDestroy++;
|
||||
|
@ -443,10 +443,14 @@ function cleanUpView(tView: TView, lView: LView): void {
|
|||
}
|
||||
|
||||
/** Removes listeners and unsubscribes from output subscriptions */
|
||||
function removeListeners(tView: TView, lView: LView): void {
|
||||
function processCleanups(tView: TView, lView: LView): void {
|
||||
const tCleanup = tView.cleanup;
|
||||
if (tCleanup !== null) {
|
||||
const lCleanup = lView[CLEANUP]!;
|
||||
// `LCleanup` contains both share information with `TCleanup` as well as instance specific
|
||||
// information appended at the end. We need to know where the end of the `TCleanup` information
|
||||
// is, and we track this with `lastLCleanupIndex`.
|
||||
let lastLCleanupIndex = -1;
|
||||
if (tCleanup !== null) {
|
||||
for (let i = 0; i < tCleanup.length - 1; i += 2) {
|
||||
if (typeof tCleanup[i] === 'string') {
|
||||
// This is a native DOM listener
|
||||
|
@ -454,7 +458,7 @@ function removeListeners(tView: TView, lView: LView): void {
|
|||
const target = typeof idxOrTargetGetter === 'function' ?
|
||||
idxOrTargetGetter(lView) :
|
||||
unwrapRNode(lView[idxOrTargetGetter]);
|
||||
const listener = lCleanup[tCleanup[i + 2]];
|
||||
const listener = lCleanup[lastLCleanupIndex = tCleanup[i + 2]];
|
||||
const useCaptureOrSubIdx = tCleanup[i + 3];
|
||||
if (typeof useCaptureOrSubIdx === 'boolean') {
|
||||
// native DOM listener registered with Renderer3
|
||||
|
@ -462,19 +466,26 @@ function removeListeners(tView: TView, lView: LView): void {
|
|||
} else {
|
||||
if (useCaptureOrSubIdx >= 0) {
|
||||
// unregister
|
||||
lCleanup[useCaptureOrSubIdx]();
|
||||
lCleanup[lastLCleanupIndex = useCaptureOrSubIdx]();
|
||||
} else {
|
||||
// Subscription
|
||||
lCleanup[-useCaptureOrSubIdx].unsubscribe();
|
||||
lCleanup[lastLCleanupIndex = -useCaptureOrSubIdx].unsubscribe();
|
||||
}
|
||||
}
|
||||
i += 2;
|
||||
} else {
|
||||
// This is a cleanup function that is grouped with the index of its context
|
||||
const context = lCleanup[tCleanup[i + 1]];
|
||||
const context = lCleanup[lastLCleanupIndex = tCleanup[i + 1]];
|
||||
tCleanup[i].call(context);
|
||||
}
|
||||
}
|
||||
if (lCleanup !== null) {
|
||||
for (let i = lastLCleanupIndex + 1; i < lCleanup.length; i++) {
|
||||
const instanceCleanupFn = lCleanup[i];
|
||||
ngDevMode && assertFunction(instanceCleanupFn, 'Expecting instance cleanup function.');
|
||||
instanceCleanupFn();
|
||||
}
|
||||
}
|
||||
lView[CLEANUP] = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,12 @@ export function assertString(actual: any, msg: string): asserts actual is string
|
|||
}
|
||||
}
|
||||
|
||||
export function assertFunction(actual: any, msg: string): asserts actual is Function {
|
||||
if (!(typeof actual === 'function')) {
|
||||
throwError(msg, actual === null ? 'null' : typeof actual, 'function', '===');
|
||||
}
|
||||
}
|
||||
|
||||
export function assertEqual<T>(actual: T, expected: T, msg: string) {
|
||||
if (!(actual == expected)) {
|
||||
throwError(msg, actual, expected, '==');
|
||||
|
|
|
@ -1436,6 +1436,9 @@
|
|||
{
|
||||
"name": "getTView"
|
||||
},
|
||||
{
|
||||
"name": "getTViewCleanup"
|
||||
},
|
||||
{
|
||||
"name": "getToken"
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue