Skip to content

Commit 0f5c800

Browse files
feat(core): introduce concept of DestroyRef (#49158)
DestroyRef represents a concept of lifecycle scope where destroy callbacks can be registered. Such callbacks are automatically executed when a given scope ends it lifecycle. In practice the most common lifecycle scopes would be represented by: - a component or en embedded view; - instance of `EnvironnementInjector`. PR Close #49158
1 parent f594725 commit 0f5c800

File tree

23 files changed

+307
-66
lines changed

23 files changed

+307
-66
lines changed

goldens/circular-deps/packages.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,6 @@
9595
"packages/core/src/render3/interfaces/definition.ts",
9696
"packages/core/src/di/r3_injector.ts"
9797
],
98-
[
99-
"packages/core/src/di/r3_injector.ts",
100-
"packages/core/src/render3/definition.ts",
101-
"packages/core/src/render3/interfaces/definition.ts"
102-
],
10398
[
10499
"packages/core/src/di/reflective_errors.ts",
105100
"packages/core/src/di/reflective_injector.ts"

goldens/public-api/core/index.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,11 @@ export const defineInjectable: typeof ɵɵdefineInjectable;
436436
// @public
437437
export function destroyPlatform(): void;
438438

439+
// @public
440+
export abstract class DestroyRef {
441+
abstract onDestroy(callback: () => void): void;
442+
}
443+
439444
// @public
440445
export interface Directive {
441446
exportAs?: string;

goldens/size-tracking/integration-payloads.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@
1919
"cli-hello-world-ivy-i18n": {
2020
"uncompressed": {
2121
"runtime": 926,
22-
"main": 125026,
22+
"main": 124522,
2323
"polyfills": 34628
2424
}
2525
},
2626
"cli-hello-world-lazy": {
2727
"uncompressed": {
2828
"runtime": 2734,
29-
"main": 228487,
29+
"main": 228996,
3030
"polyfills": 33810,
3131
"src_app_lazy_lazy_routes_ts": 487
3232
}
@@ -41,14 +41,14 @@
4141
"animations": {
4242
"uncompressed": {
4343
"runtime": 898,
44-
"main": 157699,
44+
"main": 157195,
4545
"polyfills": 33897
4646
}
4747
},
4848
"standalone-bootstrap": {
4949
"uncompressed": {
5050
"runtime": 918,
51-
"main": 85547,
51+
"main": 85043,
5252
"polyfills": 33945
5353
}
5454
},

packages/core/src/di/r3_injector.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import '../util/ng_dev_mode';
1111
import {RuntimeError, RuntimeErrorCode} from '../errors';
1212
import {OnDestroy} from '../interface/lifecycle_hooks';
1313
import {Type} from '../interface/type';
14-
import {getComponentDef} from '../render3/definition';
1514
import {FactoryFn, getFactoryDef} from '../render3/definition_factory';
1615
import {throwCyclicDependencyError, throwInvalidProviderError, throwMixedMultiProviderError} from '../render3/errors_di';
16+
import {NG_ENV_ID} from '../render3/fields';
1717
import {newArray} from '../util/array_utils';
1818
import {EMPTY_ARRAY} from '../util/empty';
1919
import {stringify} from '../util/stringify';
@@ -231,6 +231,11 @@ export class R3Injector extends EnvironmentInjector {
231231
token: ProviderToken<T>, notFoundValue: any = THROW_IF_NOT_FOUND,
232232
flags: InjectFlags|InjectOptions = InjectFlags.Default): T {
233233
this.assertNotDestroyed();
234+
235+
if (token.hasOwnProperty(NG_ENV_ID)) {
236+
return (token as any)[NG_ENV_ID](this);
237+
}
238+
234239
flags = convertToBitFlags(flags) as InjectFlags;
235240

236241
// Set the injection context.

packages/core/src/linker.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
export {Compiler, COMPILER_OPTIONS, CompilerFactory, CompilerOptions, ModuleWithComponentFactories} from './linker/compiler';
1111
export {ComponentFactory, ComponentRef} from './linker/component_factory';
1212
export {ComponentFactoryResolver} from './linker/component_factory_resolver';
13+
export {DestroyRef} from './linker/destroy_ref';
1314
export {ElementRef} from './linker/element_ref';
1415
export {NgModuleFactory, NgModuleRef} from './linker/ng_module_factory';
1516
export {getModuleFactory, getNgModuleById} from './linker/ng_module_factory_loader';
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {EnvironmentInjector} from '../di';
10+
import {LView} from '../render3/interfaces/view';
11+
import {getLView} from '../render3/state';
12+
import {storeLViewOnDestroy} from '../render3/util/view_utils';
13+
14+
/**
15+
* `DestroyRef` lets you set callbacks to run for any cleanup or destruction behavior.
16+
* The scope of this destruction depends on where `DestroyRef` is injected. If `DestroyRef`
17+
* is injected in a component or directive, the callbacks run when that component or
18+
* directive is destroyed. Otherwise the callbacks run when a corresponding injector is destroyed.
19+
*/
20+
export abstract class DestroyRef {
21+
// Here the `DestroyRef` acts primarily as a DI token. There are (currently) types of objects that
22+
// can be returned from the injector when asking for this token:
23+
// - `NodeInjectorDestroyRef` when retrieved from a node injector;
24+
// - `EnvironmentInjector` when retrieved from an environment injector
25+
26+
/**
27+
* Registers a destroy callback in a given lifecycle scope.
28+
*/
29+
abstract onDestroy(callback: () => void): void;
30+
31+
/**
32+
* @internal
33+
* @nocollapse
34+
*/
35+
static __NG_ELEMENT_ID__: () => DestroyRef = injectDestroyRef;
36+
37+
/**
38+
* @internal
39+
* @nocollapse
40+
*/
41+
static __NG_ENV_ID__: (injector: EnvironmentInjector) => DestroyRef = (injector) => injector;
42+
}
43+
44+
class NodeInjectorDestroyRef extends DestroyRef {
45+
constructor(private _lView: LView) {
46+
super();
47+
}
48+
49+
override onDestroy(callback: () => void): void {
50+
storeLViewOnDestroy(this._lView, callback);
51+
}
52+
}
53+
54+
function injectDestroyRef(): DestroyRef {
55+
return new NodeInjectorDestroyRef(getLView());
56+
}

packages/core/src/render3/fields.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,13 @@ export const NG_FACTORY_DEF = getClosureSafeProperty({ɵfac: getClosureSafePrope
2121
*/
2222
// TODO(misko): This is wrong. The NG_ELEMENT_ID should never be minified.
2323
export const NG_ELEMENT_ID = getClosureSafeProperty({__NG_ELEMENT_ID__: getClosureSafeProperty});
24+
25+
/**
26+
* The `NG_ENV_ID` field on a DI token indicates special processing in the `EnvironmentInjector`:
27+
* getting such tokens from the `EnvironmentInjector` will bypass the standard DI resolution
28+
* strategy and instead will return implementation produced by the `NG_ENV_ID` factory function.
29+
*
30+
* This particular retrieval of DI tokens is mostly done to eliminate circular dependencies and
31+
* improve tree-shaking.
32+
*/
33+
export const NG_ENV_ID = getClosureSafeProperty({__NG_ENV_ID__: getClosureSafeProperty});

packages/core/src/render3/instructions/shared.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,16 @@ import {Renderer, RendererFactory} from '../interfaces/renderer';
3333
import {RComment, RElement, RNode, RText} from '../interfaces/renderer_dom';
3434
import {SanitizerFn} from '../interfaces/sanitization';
3535
import {isComponentDef, isComponentHost, isContentQueryHost, isRootView} from '../interfaces/type_checks';
36-
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, EMBEDDED_VIEW_INJECTOR, FLAGS, HEADER_OFFSET, HOST, HostBindingOpCodes, ID, InitPhaseState, INJECTOR, LView, LViewFlags, NEXT, PARENT, RENDERER, RENDERER_FACTORY, SANITIZER, T_HOST, TData, TRANSPLANTED_VIEWS_TO_REFRESH, TVIEW, TView, TViewType} from '../interfaces/view';
36+
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, EMBEDDED_VIEW_INJECTOR, FLAGS, HEADER_OFFSET, HOST, HostBindingOpCodes, ID, InitPhaseState, INJECTOR, LView, LViewFlags, NEXT, ON_DESTROY_HOOKS, PARENT, RENDERER, RENDERER_FACTORY, SANITIZER, T_HOST, TData, TRANSPLANTED_VIEWS_TO_REFRESH, TVIEW, TView, TViewType} from '../interfaces/view';
3737
import {assertPureTNodeType, assertTNodeType} from '../node_assert';
3838
import {updateTextNode} from '../node_manipulation';
3939
import {isInlineTemplate, isNodeMatchingSelectorList} from '../node_selector_matcher';
4040
import {profiler, ProfilerEvent} from '../profiler';
41-
import {enterView, getBindingsEnabled, getCurrentDirectiveIndex, getCurrentParentTNode, getCurrentTNode, getCurrentTNodePlaceholderOk, getSelectedIndex, isCurrentTNodeParent, isInCheckNoChangesMode, isInI18nBlock, leaveView, setBindingIndex, setBindingRootForHostBindings, setCurrentDirectiveIndex, setCurrentQueryIndex, setCurrentTNode, setIsInCheckNoChangesMode, setSelectedIndex} from '../state';
41+
import {enterView, getBindingsEnabled, getCurrentDirectiveIndex, getCurrentParentTNode, getCurrentTNodePlaceholderOk, getSelectedIndex, isCurrentTNodeParent, isInCheckNoChangesMode, isInI18nBlock, leaveView, setBindingIndex, setBindingRootForHostBindings, setCurrentDirectiveIndex, setCurrentQueryIndex, setCurrentTNode, setIsInCheckNoChangesMode, setSelectedIndex} from '../state';
4242
import {NO_CHANGE} from '../tokens';
4343
import {mergeHostAttrs} from '../util/attrs_utils';
4444
import {INTERPOLATION_DELIMITER} from '../util/misc_utils';
45-
import {renderStringify, stringifyForError} from '../util/stringify_utils';
45+
import {renderStringify} from '../util/stringify_utils';
4646
import {getFirstLContainer, getLViewParent, getNextLContainer} from '../util/view_traversal_utils';
4747
import {getComponentLViewByIndex, getNativeByIndex, getNativeByTNode, isCreationMode, resetPreOrderHookFlags, unwrapLView, updateTransplantedViewCount, viewAttachedToChangeDetector} from '../util/view_utils';
4848

@@ -676,26 +676,28 @@ export function locateHostElement(
676676
* On the first template pass, saves in TView:
677677
* - Cleanup function
678678
* - Index of context we just saved in LView.cleanupInstances
679-
*
680-
* This function can also be used to store instance specific cleanup fns. In that case the `context`
681-
* is `null` and the function is store in `LView` (rather than it `TView`).
682679
*/
683680
export function storeCleanupWithContext(
684681
tView: TView, lView: LView, context: any, cleanupFn: Function): void {
685682
const lCleanup = getOrCreateLViewCleanup(lView);
686-
if (context === null) {
687-
// If context is null that this is instance specific callback. These callbacks can only be
688-
// inserted after template shared instances. For this reason in ngDevMode we freeze the TView.
683+
684+
// Historically the `storeCleanupWithContext` was used to register both framework-level and
685+
// user-defined cleanup callbacks, but over time those two types of cleanups were separated. This
686+
// dev mode checks assures that user-level cleanup callbacks are _not_ stored in data structures
687+
// reserved for framework-specific hooks.
688+
ngDevMode &&
689+
assertDefined(
690+
context, 'Cleanup context is mandatory when registering framework-level destroy hooks');
691+
lCleanup.push(context);
692+
693+
if (tView.firstCreatePass) {
694+
getOrCreateTViewCleanup(tView).push(cleanupFn, lCleanup.length - 1);
695+
} else {
696+
// Make sure that no new framework-level cleanup functions are registered after the first
697+
// template pass is done (and TView data structures are meant to fully constructed).
689698
if (ngDevMode) {
690699
Object.freeze(getOrCreateTViewCleanup(tView));
691700
}
692-
lCleanup.push(cleanupFn);
693-
} else {
694-
lCleanup.push(context);
695-
696-
if (tView.firstCreatePass) {
697-
getOrCreateTViewCleanup(tView).push(cleanupFn, lCleanup.length - 1);
698-
}
699701
}
700702
}
701703

packages/core/src/render3/interfaces/definition.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import {SchemaMetadata} from '../../metadata/schema';
1313
import {ViewEncapsulation} from '../../metadata/view';
1414
import {FactoryFn} from '../definition_factory';
1515

16-
import {TAttributes, TConstantsOrFactory, TContainerNode, TElementContainerNode, TElementNode} from './node';
16+
import {TAttributes, TConstantsOrFactory} from './node';
1717
import {CssSelectorList} from './projection';
18-
import {LView, TView} from './view';
18+
import {TView} from './view';
1919

2020

2121
/**

packages/core/src/render3/interfaces/view.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,15 @@ export const PREORDER_HOOK_FLAGS = 18;
4848
export const QUERIES = 19;
4949
export const ID = 20;
5050
export const EMBEDDED_VIEW_INJECTOR = 21;
51+
export const ON_DESTROY_HOOKS = 22;
5152
/**
5253
* Size of LView's header. Necessary to adjust for it when setting slots.
5354
*
5455
* IMPORTANT: `HEADER_OFFSET` should only be referred to the in the `ɵɵ*` instructions to translate
5556
* instruction index into `LView` index. All other indexes should be in the `LView` index space and
5657
* there should be no need to refer to `HEADER_OFFSET` anywhere else.
5758
*/
58-
export const HEADER_OFFSET = 22;
59+
export const HEADER_OFFSET = 23;
5960

6061

6162
// This interface replaces the real LView interface if it is an arg or a
@@ -327,6 +328,13 @@ export interface LView<T = unknown> extends Array<any> {
327328
* precedence over the element and module injectors.
328329
*/
329330
readonly[EMBEDDED_VIEW_INJECTOR]: Injector|null;
331+
332+
/**
333+
* A collection of callbacks functions that are executed when a given LView is destroyed. Those
334+
* are user defined, LView-specific destroy callbacks that don't have any corresponding TView
335+
* entries.
336+
*/
337+
[ON_DESTROY_HOOKS]: Array<() => void>|null;
330338
}
331339

332340
/** Flags associated with an LView (saved in LView[FLAGS]) */

0 commit comments

Comments
 (0)