Skip to content

Commit 6af0880

Browse files
committed
fix(core): Allow modification of lifecycle hooks any time before bootstrap
Currently we read lifecycle hooks eagerly during `ɵɵdefineComponent`. The result is that it is not possible to do any sort of meta-programing such as mixins or adding lifecycle hooks using custom decorators since any such code executes after `ɵɵdefineComponent` has extracted the lifecycle hooks from the prototype. Additionally the behavior is inconsistent between AOT and JIT mode. In JIT mode overriding lifecycle hooks is possible because the whole `ɵɵdefineComponent` is placed in getter which is executed lazily. This is because JIT mode must compile a template which can be specified as `templateURL` and those we are waiting for its resolution. - `+` `ɵɵdefineComponent` becomes smaller as it no longer needs to copy lifecycle hooks from prototype to `ComponentDef` - `-` `ɵɵNgOnChangesFeature` feature is now always included with the codebase as it is no longer tree shakable. Previously we have read lifecycle hooks from prototype in the `ɵɵdefineComponent` so that lifecycle hook access would be monomorphic. This decision was made before we had `T*` data structures. By not reading the lifecycle hooks we are moving the megamorhic read form `ɵɵdefineComponent` to instructions. However, the reads happen on `firstTemplatePass` only and are subsequently cached in the `T*` data structures. The result is that the overall performance should be same (or slightly better as the intermediate `ComponentDef` has been removed.) - [ ] Remove `ɵɵNgOnChangesFeature` from compiler. (It will no longer be a feature.) - [ ] Discuss the future of `Features` as they hinder meta-programing. Fix angular#30497
1 parent e9cb6fb commit 6af0880

File tree

13 files changed

+265
-148
lines changed

13 files changed

+265
-148
lines changed

goldens/public-api/core/core.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -894,7 +894,7 @@ export declare function ɵɵnextContext<T = any>(level?: number): T;
894894

895895
export declare type ɵɵNgModuleDefWithMeta<T, Declarations, Imports, Exports> = ɵNgModuleDef<T>;
896896

897-
export declare function ɵɵNgOnChangesFeature<T>(definition: ɵDirectiveDef<T>): void;
897+
export declare function ɵɵNgOnChangesFeature<T>(): DirectiveDefFeature;
898898

899899
export declare function ɵɵpipe(index: number, pipeName: string): any;
900900

@@ -1056,7 +1056,7 @@ export declare function ɵɵstylePropInterpolateV(prop: string, values: any[], v
10561056

10571057
export declare function ɵɵtemplate(index: number, templateFn: ComponentTemplate<any> | null, decls: number, vars: number, tagName?: string | null, attrsIndex?: number | null, localRefsIndex?: number | null, localRefExtractor?: LocalRefExtractor): void;
10581058

1059-
export declare function ɵɵtemplateRefExtractor(tNode: TNode, currentView: ɵangular_packages_core_core_bo): TemplateRef<unknown> | null;
1059+
export declare function ɵɵtemplateRefExtractor(tNode: TNode, currentView: ɵangular_packages_core_core_bp): TemplateRef<unknown> | null;
10601060

10611061
export declare function ɵɵtext(index: number, value?: string): void;
10621062

goldens/size-tracking/aio-payloads.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"master": {
1313
"uncompressed": {
1414
"runtime-es2015": 2987,
15-
"main-es2015": 450883,
15+
"main-es2015": 450301,
1616
"polyfills-es2015": 52630
1717
}
1818
}
@@ -26,4 +26,4 @@
2626
}
2727
}
2828
}
29-
}
29+
}

goldens/size-tracking/integration-payloads.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,16 @@
1212
"master": {
1313
"uncompressed": {
1414
"runtime-es2015": 1485,
15-
"main-es2015": 16959,
16-
"polyfills-es2015": 36938
15+
"main-es2015": 17362,
16+
"polyfills-es2015": 36657
1717
}
1818
}
1919
},
2020
"cli-hello-world-ivy-compat": {
2121
"master": {
2222
"uncompressed": {
2323
"runtime-es2015": 1485,
24-
"main-es2015": 147314,
24+
"main-es2015": 147573,
2525
"polyfills-es2015": 36571
2626
}
2727
}
@@ -30,7 +30,7 @@
3030
"master": {
3131
"uncompressed": {
3232
"runtime-es2015": 1485,
33-
"main-es2015": 135533,
33+
"main-es2015": 136168,
3434
"polyfills-es2015": 37248
3535
}
3636
}
@@ -66,4 +66,4 @@
6666
}
6767
}
6868
}
69-
}
69+
}

packages/core/src/render3/definition.ts

Lines changed: 59 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -290,72 +290,65 @@ export function ɵɵdefineComponent<T>(componentDefinition: {
290290
schemas?: SchemaMetadata[] | null;
291291
}): never {
292292
return noSideEffects(() => {
293-
// Initialize ngDevMode. This must be the first statement in ɵɵdefineComponent.
294-
// See the `initNgDevMode` docstring for more information.
295-
(typeof ngDevMode === 'undefined' || ngDevMode) && initNgDevMode();
296-
297-
const type = componentDefinition.type;
298-
const typePrototype = type.prototype;
299-
const declaredInputs: {[key: string]: string} = {} as any;
300-
const def: Mutable<ComponentDef<any>, keyof ComponentDef<any>> = {
301-
type: type,
302-
providersResolver: null,
303-
decls: componentDefinition.decls,
304-
vars: componentDefinition.vars,
305-
factory: null,
306-
template: componentDefinition.template || null!,
307-
consts: componentDefinition.consts || null,
308-
ngContentSelectors: componentDefinition.ngContentSelectors,
309-
hostBindings: componentDefinition.hostBindings || null,
310-
hostVars: componentDefinition.hostVars || 0,
311-
hostAttrs: componentDefinition.hostAttrs || null,
312-
contentQueries: componentDefinition.contentQueries || null,
313-
declaredInputs: declaredInputs,
314-
inputs: null!, // assigned in noSideEffects
315-
outputs: null!, // assigned in noSideEffects
316-
exportAs: componentDefinition.exportAs || null,
317-
onChanges: null,
318-
onInit: typePrototype.ngOnInit || null,
319-
doCheck: typePrototype.ngDoCheck || null,
320-
afterContentInit: typePrototype.ngAfterContentInit || null,
321-
afterContentChecked: typePrototype.ngAfterContentChecked || null,
322-
afterViewInit: typePrototype.ngAfterViewInit || null,
323-
afterViewChecked: typePrototype.ngAfterViewChecked || null,
324-
onDestroy: typePrototype.ngOnDestroy || null,
325-
onPush: componentDefinition.changeDetection === ChangeDetectionStrategy.OnPush,
326-
directiveDefs: null!, // assigned in noSideEffects
327-
pipeDefs: null!, // assigned in noSideEffects
328-
selectors: componentDefinition.selectors || EMPTY_ARRAY,
329-
viewQuery: componentDefinition.viewQuery || null,
330-
features: componentDefinition.features as DirectiveDefFeature[] || null,
331-
data: componentDefinition.data || {},
332-
// TODO(misko): convert ViewEncapsulation into const enum so that it can be used directly in
333-
// the next line. Also `None` should be 0 not 2.
334-
encapsulation: componentDefinition.encapsulation || ViewEncapsulation.Emulated,
335-
id: 'c',
336-
styles: componentDefinition.styles || EMPTY_ARRAY,
337-
_: null as never,
338-
setInput: null,
339-
schemas: componentDefinition.schemas || null,
340-
tView: null,
341-
};
342-
const directiveTypes = componentDefinition.directives!;
343-
const feature = componentDefinition.features;
344-
const pipeTypes = componentDefinition.pipes!;
345-
def.id += _renderCompCount++;
346-
def.inputs = invertObject(componentDefinition.inputs, declaredInputs),
347-
def.outputs = invertObject(componentDefinition.outputs),
348-
feature && feature.forEach((fn) => fn(def));
349-
def.directiveDefs = directiveTypes ?
350-
() => (typeof directiveTypes === 'function' ? directiveTypes() : directiveTypes)
351-
.map(extractDirectiveDef) :
352-
null;
353-
def.pipeDefs = pipeTypes ?
354-
() => (typeof pipeTypes === 'function' ? pipeTypes() : pipeTypes).map(extractPipeDef) :
355-
null;
356-
357-
return def as never;
358-
});
293+
// Initialize ngDevMode. This must be the first statement in ɵɵdefineComponent.
294+
// See the `initNgDevMode` docstring for more information.
295+
(typeof ngDevMode === 'undefined' || ngDevMode) && initNgDevMode();
296+
297+
const type = componentDefinition.type;
298+
const typePrototype = type.prototype;
299+
const declaredInputs: {[key: string]: string} = {} as any;
300+
const def: Mutable<ComponentDef<any>, keyof ComponentDef<any>> = {
301+
type: type,
302+
providersResolver: null,
303+
decls: componentDefinition.decls,
304+
vars: componentDefinition.vars,
305+
factory: null,
306+
template: componentDefinition.template || null!,
307+
consts: componentDefinition.consts || null,
308+
ngContentSelectors: componentDefinition.ngContentSelectors,
309+
hostBindings: componentDefinition.hostBindings || null,
310+
hostVars: componentDefinition.hostVars || 0,
311+
hostAttrs: componentDefinition.hostAttrs || null,
312+
contentQueries: componentDefinition.contentQueries || null,
313+
declaredInputs: declaredInputs,
314+
inputs: null!, // assigned in noSideEffects
315+
outputs: null!, // assigned in noSideEffects
316+
exportAs: componentDefinition.exportAs || null,
317+
onPush: componentDefinition.changeDetection === ChangeDetectionStrategy.OnPush,
318+
directiveDefs: null!, // assigned in noSideEffects
319+
pipeDefs: null!, // assigned in noSideEffects
320+
selectors: componentDefinition.selectors || EMPTY_ARRAY,
321+
viewQuery: componentDefinition.viewQuery || null,
322+
features: componentDefinition.features as DirectiveDefFeature[] || null,
323+
data: componentDefinition.data || {},
324+
// TODO(misko): convert ViewEncapsulation into const enum so that it can be used
325+
// directly in the next line. Also `None` should be 0 not 2.
326+
encapsulation: componentDefinition.encapsulation || ViewEncapsulation.Emulated,
327+
id: 'c',
328+
styles: componentDefinition.styles || EMPTY_ARRAY,
329+
_: null as never,
330+
setInput: null,
331+
schemas: componentDefinition.schemas || null,
332+
tView: null,
333+
};
334+
const directiveTypes = componentDefinition.directives!;
335+
const feature = componentDefinition.features;
336+
const pipeTypes = componentDefinition.pipes!;
337+
def.id += _renderCompCount++;
338+
def.inputs = invertObject(componentDefinition.inputs, declaredInputs),
339+
def.outputs = invertObject(componentDefinition.outputs),
340+
feature && feature.forEach((fn) => fn(def));
341+
def.directiveDefs = directiveTypes ?
342+
() => (typeof directiveTypes === 'function' ? directiveTypes() : directiveTypes)
343+
.map(extractDirectiveDef) :
344+
null;
345+
def.pipeDefs = pipeTypes ?
346+
() =>
347+
(typeof pipeTypes === 'function' ? pipeTypes() : pipeTypes).map(extractPipeDef) :
348+
null;
349+
350+
return def as never;
351+
}) as never;
359352
}
360353

361354
/**

packages/core/src/render3/features/inherit_definition_feature.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,17 +78,6 @@ export function ɵɵInheritDefinitionFeature(definition: DirectiveDef<any>|Compo
7878
const defData = (definition as ComponentDef<any>).data;
7979
defData.animation = (defData.animation || []).concat(superDef.data.animation);
8080
}
81-
82-
// Inherit hooks
83-
// Assume super class inheritance feature has already run.
84-
writeableDef.afterContentChecked =
85-
writeableDef.afterContentChecked || superDef.afterContentChecked;
86-
writeableDef.afterContentInit = definition.afterContentInit || superDef.afterContentInit;
87-
writeableDef.afterViewChecked = definition.afterViewChecked || superDef.afterViewChecked;
88-
writeableDef.afterViewInit = definition.afterViewInit || superDef.afterViewInit;
89-
writeableDef.doCheck = definition.doCheck || superDef.doCheck;
90-
writeableDef.onDestroy = definition.onDestroy || superDef.onDestroy;
91-
writeableDef.onInit = definition.onInit || superDef.onInit;
9281
}
9382

9483
// Run parent features

packages/core/src/render3/features/ng_onchanges_feature.ts

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,6 @@ import {SimpleChange, SimpleChanges} from '../../interface/simple_change';
1111
import {EMPTY_OBJ} from '../empty';
1212
import {DirectiveDef, DirectiveDefFeature} from '../interfaces/definition';
1313

14-
const PRIVATE_PREFIX = '__ngOnChanges_';
15-
16-
type OnChangesExpando = OnChanges&{
17-
__ngOnChanges_: SimpleChanges|null|undefined;
18-
// tslint:disable-next-line:no-any Can hold any value
19-
[key: string]: any;
20-
};
21-
2214
/**
2315
* The NgOnChangesFeature decorates a component with support for the ngOnChanges
2416
* lifecycle hook, so it should be included in any component that implements
@@ -41,12 +33,15 @@ type OnChangesExpando = OnChanges&{
4133
*
4234
* @codeGenApi
4335
*/
36+
export function ɵɵNgOnChangesFeature<T>(): DirectiveDefFeature {
37+
return NgOnChangesFeatureImpl;
38+
}
4439

45-
export function ɵɵNgOnChangesFeature<T>(definition: DirectiveDef<T>): void {
40+
export function NgOnChangesFeatureImpl<T>(definition: DirectiveDef<T>) {
4641
if (definition.type.prototype.ngOnChanges) {
4742
definition.setInput = ngOnChangesSetInput;
48-
(definition as {onChanges: Function}).onChanges = wrapOnChanges();
4943
}
44+
return rememberChangeHistoryAndInvokeOnChangesHook;
5045
}
5146

5247
// This option ensures that the ngOnChanges lifecycle hook will be inherited
@@ -55,28 +50,37 @@ export function ɵɵNgOnChangesFeature<T>(definition: DirectiveDef<T>): void {
5550
// tslint:disable-next-line:no-toplevel-property-access
5651
(ɵɵNgOnChangesFeature as DirectiveDefFeature).ngInherit = true;
5752

58-
function wrapOnChanges() {
59-
return function wrapOnChangesHook_inPreviousChangesStorage(this: OnChanges) {
60-
const simpleChangesStore = getSimpleChangesStore(this);
61-
const current = simpleChangesStore && simpleChangesStore.current;
53+
/**
54+
* This is a synthetic lifecycle hook which gets inserted into `TView.preOrderHooks` to simulate
55+
* `ngOnChanges`.
56+
*
57+
* The hook reads the `NgSimpleChangesStore` data from the component instance and if changes are
58+
* found it invokes `ngOnChanges` on the component instance.
59+
*
60+
* @param this Component instance. Because this function gets inserted into `TView.preOrderHooks`,
61+
* it is guaranteed to be called with component instance.
62+
*/
63+
function rememberChangeHistoryAndInvokeOnChangesHook(this: OnChanges) {
64+
const simpleChangesStore = getSimpleChangesStore(this);
65+
const current = simpleChangesStore?.current;
6266

63-
if (current) {
64-
const previous = simpleChangesStore!.previous;
65-
if (previous === EMPTY_OBJ) {
66-
simpleChangesStore!.previous = current;
67-
} else {
68-
// New changes are copied to the previous store, so that we don't lose history for inputs
69-
// which were not changed this time
70-
for (let key in current) {
71-
previous[key] = current[key];
72-
}
67+
if (current) {
68+
const previous = simpleChangesStore!.previous;
69+
if (previous === EMPTY_OBJ) {
70+
simpleChangesStore!.previous = current;
71+
} else {
72+
// New changes are copied to the previous store, so that we don't lose history for inputs
73+
// which were not changed this time
74+
for (let key in current) {
75+
previous[key] = current[key];
7376
}
74-
simpleChangesStore!.current = null;
75-
this.ngOnChanges(current);
7677
}
77-
};
78+
simpleChangesStore!.current = null;
79+
this.ngOnChanges(current);
80+
}
7881
}
7982

83+
8084
function ngOnChangesSetInput<T>(
8185
this: DirectiveDef<T>, instance: T, value: any, publicName: string, privateName: string): void {
8286
const simpleChangesStore = getSimpleChangesStore(instance) ||
@@ -102,6 +106,10 @@ function setSimpleChangesStore(instance: any, store: NgSimpleChangesStore): NgSi
102106
return instance[SIMPLE_CHANGES_STORE] = store;
103107
}
104108

109+
/**
110+
* Data structure which is monkey-patched on the component instance and used by `ngOnChanges`
111+
* life-cycle hook to track previous input values.
112+
*/
105113
interface NgSimpleChangesStore {
106114
previous: SimpleChanges;
107115
current: SimpleChanges|null;

0 commit comments

Comments
 (0)