Skip to content

Commit d83307a

Browse files
marclavaljasonaden
authored andcommitted
fix(ivy): init hooks should be called once and only once (angular#28239)
PR Close angular#28239
1 parent 8737506 commit d83307a

File tree

9 files changed

+240
-191
lines changed

9 files changed

+240
-191
lines changed

packages/core/src/render3/definition.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ export function defineComponent<T>(componentDefinition: {
256256
inputs: null !, // assigned in noSideEffects
257257
outputs: null !, // assigned in noSideEffects
258258
exportAs: componentDefinition.exportAs || null,
259+
onChanges: null,
259260
onInit: typePrototype.ngOnInit || null,
260261
doCheck: typePrototype.ngDoCheck || null,
261262
afterContentInit: typePrototype.ngAfterContentInit || null,

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

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,11 @@ export function NgOnChangesFeature<T>(): DirectiveDefFeature {
4949
function NgOnChangesFeatureImpl<T>(definition: DirectiveDef<T>): void {
5050
if (definition.type.prototype.ngOnChanges) {
5151
definition.setInput = ngOnChangesSetInput;
52-
53-
const prevDoCheck = definition.doCheck;
54-
const prevOnInit = definition.onInit;
55-
56-
definition.onInit = wrapOnChanges(prevOnInit);
57-
definition.doCheck = wrapOnChanges(prevDoCheck);
52+
definition.onChanges = wrapOnChanges();
5853
}
5954
}
6055

61-
function wrapOnChanges(hook: (() => void) | null) {
56+
function wrapOnChanges() {
6257
return function(this: OnChanges) {
6358
const simpleChangesStore = getSimpleChangesStore(this);
6459
const current = simpleChangesStore && simpleChangesStore.current;
@@ -68,8 +63,6 @@ function wrapOnChanges(hook: (() => void) | null) {
6863
simpleChangesStore !.current = null;
6964
this.ngOnChanges(current);
7065
}
71-
72-
hook && hook.call(this);
7366
};
7467
}
7568

packages/core/src/render3/hooks.ts

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {SimpleChanges} from '../interface/simple_change';
109
import {assertEqual} from '../util/assert';
1110

1211
import {DirectiveDef} from './interfaces/definition';
1312
import {TNode} from './interfaces/node';
14-
import {FLAGS, HookData, LView, LViewFlags, TView} from './interfaces/view';
13+
import {FLAGS, HookData, InitPhaseState, LView, LViewFlags, TView} from './interfaces/view';
1514

1615

1716

@@ -34,10 +33,15 @@ export function registerPreOrderHooks(
3433
ngDevMode &&
3534
assertEqual(tView.firstTemplatePass, true, 'Should only be called on first template pass');
3635

37-
const {onInit, doCheck} = directiveDef;
36+
const {onChanges, onInit, doCheck} = directiveDef;
37+
38+
if (onChanges) {
39+
(tView.initHooks || (tView.initHooks = [])).push(directiveIndex, onChanges);
40+
(tView.checkHooks || (tView.checkHooks = [])).push(directiveIndex, onChanges);
41+
}
3842

3943
if (onInit) {
40-
(tView.initHooks || (tView.initHooks = [])).push(directiveIndex, onInit);
44+
(tView.initHooks || (tView.initHooks = [])).push(-directiveIndex, onInit);
4145
}
4246

4347
if (doCheck) {
@@ -73,7 +77,7 @@ export function registerPostOrderHooks(tView: TView, tNode: TNode): void {
7377
for (let i = tNode.directiveStart, end = tNode.directiveEnd; i < end; i++) {
7478
const directiveDef = tView.data[i] as DirectiveDef<any>;
7579
if (directiveDef.afterContentInit) {
76-
(tView.contentHooks || (tView.contentHooks = [])).push(i, directiveDef.afterContentInit);
80+
(tView.contentHooks || (tView.contentHooks = [])).push(-i, directiveDef.afterContentInit);
7781
}
7882

7983
if (directiveDef.afterContentChecked) {
@@ -83,7 +87,7 @@ export function registerPostOrderHooks(tView: TView, tNode: TNode): void {
8387
}
8488

8589
if (directiveDef.afterViewInit) {
86-
(tView.viewHooks || (tView.viewHooks = [])).push(i, directiveDef.afterViewInit);
90+
(tView.viewHooks || (tView.viewHooks = [])).push(-i, directiveDef.afterViewInit);
8791
}
8892

8993
if (directiveDef.afterViewChecked) {
@@ -114,9 +118,10 @@ export function registerPostOrderHooks(tView: TView, tNode: TNode): void {
114118
*/
115119
export function executeInitHooks(
116120
currentView: LView, tView: TView, checkNoChangesMode: boolean): void {
117-
if (!checkNoChangesMode && currentView[FLAGS] & LViewFlags.RunInit) {
118-
executeHooks(currentView, tView.initHooks, tView.checkHooks, checkNoChangesMode);
119-
currentView[FLAGS] &= ~LViewFlags.RunInit;
121+
if (!checkNoChangesMode) {
122+
executeHooks(
123+
currentView, tView.initHooks, tView.checkHooks, checkNoChangesMode,
124+
InitPhaseState.OnInitHooksToBeRun);
120125
}
121126
}
122127

@@ -131,12 +136,19 @@ export function executeInitHooks(
131136
*/
132137
export function executeHooks(
133138
currentView: LView, firstPassHooks: HookData | null, checkHooks: HookData | null,
134-
checkNoChangesMode: boolean): void {
139+
checkNoChangesMode: boolean, initPhase: number): void {
135140
if (checkNoChangesMode) return;
136-
137-
const hooksToCall = currentView[FLAGS] & LViewFlags.FirstLViewPass ? firstPassHooks : checkHooks;
141+
const hooksToCall = (currentView[FLAGS] & LViewFlags.InitPhaseStateMask) === initPhase ?
142+
firstPassHooks :
143+
checkHooks;
138144
if (hooksToCall) {
139-
callHooks(currentView, hooksToCall);
145+
callHooks(currentView, hooksToCall, initPhase);
146+
}
147+
// The init phase state must be always checked here as it may have been recursively updated
148+
if ((currentView[FLAGS] & LViewFlags.InitPhaseStateMask) === initPhase &&
149+
initPhase !== InitPhaseState.InitPhaseCompleted) {
150+
currentView[FLAGS] &= LViewFlags.IndexWithinInitPhaseReset;
151+
currentView[FLAGS] += LViewFlags.InitPhaseStateIncrementer;
140152
}
141153
}
142154

@@ -147,8 +159,24 @@ export function executeHooks(
147159
* @param currentView The current view
148160
* @param arr The array in which the hooks are found
149161
*/
150-
export function callHooks(currentView: LView, arr: HookData): void {
162+
export function callHooks(currentView: LView, arr: HookData, initPhase?: number): void {
163+
let initHooksCount = 0;
151164
for (let i = 0; i < arr.length; i += 2) {
152-
(arr[i + 1] as() => void).call(currentView[arr[i] as number]);
165+
const isInitHook = arr[i] < 0;
166+
const directiveIndex = isInitHook ? -arr[i] : arr[i] as number;
167+
const directive = currentView[directiveIndex];
168+
const hook = arr[i + 1] as() => void;
169+
if (isInitHook) {
170+
initHooksCount++;
171+
const indexWithintInitPhase = currentView[FLAGS] >> LViewFlags.IndexWithinInitPhaseShift;
172+
// The init phase state must be always checked here as it may have been recursively updated
173+
if (indexWithintInitPhase < initHooksCount &&
174+
(currentView[FLAGS] & LViewFlags.InitPhaseStateMask) === initPhase) {
175+
currentView[FLAGS] += LViewFlags.IndexWithinInitPhaseIncrementer;
176+
hook.call(directive);
177+
}
178+
} else {
179+
hook.call(directive);
180+
}
153181
}
154182
}

packages/core/src/render3/instructions.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'
3232
import {LQueries} from './interfaces/query';
3333
import {GlobalTargetResolver, ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, isProceduralRenderer} from './interfaces/renderer';
3434
import {SanitizerFn} from './interfaces/sanitization';
35-
import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, INJECTOR, LView, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TAIL, TData, TVIEW, TView} from './interfaces/view';
35+
import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, INJECTOR, InitPhaseState, LView, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TAIL, TData, TVIEW, TView} from './interfaces/view';
3636
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
3737
import {appendChild, appendProjectedNode, createTextNode, getLViewChild, insertView, removeView} from './node_manipulation';
3838
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
@@ -83,7 +83,9 @@ export function refreshDescendantViews(lView: LView) {
8383
// Content query results must be refreshed before content hooks are called.
8484
refreshContentQueries(tView);
8585

86-
executeHooks(lView, tView.contentHooks, tView.contentCheckHooks, checkNoChangesMode);
86+
executeHooks(
87+
lView, tView.contentHooks, tView.contentCheckHooks, checkNoChangesMode,
88+
InitPhaseState.AfterContentInitHooksToBeRun);
8789

8890
setHostBindings(tView, lView);
8991
}
@@ -159,8 +161,7 @@ export function createLView<T>(
159161
rendererFactory?: RendererFactory3 | null, renderer?: Renderer3 | null,
160162
sanitizer?: Sanitizer | null, injector?: Injector | null): LView {
161163
const lView = tView.blueprint.slice() as LView;
162-
lView[FLAGS] = flags | LViewFlags.CreationMode | LViewFlags.Attached | LViewFlags.RunInit |
163-
LViewFlags.FirstLViewPass;
164+
lView[FLAGS] = flags | LViewFlags.CreationMode | LViewFlags.Attached | LViewFlags.FirstLViewPass;
164165
lView[PARENT] = lView[DECLARATION_VIEW] = parentLView;
165166
lView[CONTEXT] = context;
166167
lView[RENDERER_FACTORY] = (rendererFactory || parentLView && parentLView[RENDERER_FACTORY]) !;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ export interface DirectiveDef<T> extends BaseDef<T> {
140140
hostBindings: HostBindingsFunction<T>|null;
141141

142142
/* The following are lifecycle hooks for this component */
143+
onChanges: (() => void)|null;
143144
onInit: (() => void)|null;
144145
doCheck: (() => void)|null;
145146
afterContentInit: (() => void)|null;

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

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,10 @@ export interface LView extends Array<any> {
215215

216216
/** Flags associated with an LView (saved in LView[FLAGS]) */
217217
export const enum LViewFlags {
218+
/** The state of the init phase on the first 2 bits */
219+
InitPhaseStateIncrementer = 0b00000000001,
220+
InitPhaseStateMask = 0b00000000011,
221+
218222
/**
219223
* Whether or not the view is in creationMode.
220224
*
@@ -223,7 +227,7 @@ export const enum LViewFlags {
223227
* back into the parent view, `data` will be defined and `creationMode` will be
224228
* improperly reported as false.
225229
*/
226-
CreationMode = 0b000000001,
230+
CreationMode = 0b00000000100,
227231

228232
/**
229233
* Whether or not this LView instance is on its first processing pass.
@@ -232,31 +236,43 @@ export const enum LViewFlags {
232236
* has completed one creation mode run and one update mode run. At this
233237
* time, the flag is turned off.
234238
*/
235-
FirstLViewPass = 0b000000010,
239+
FirstLViewPass = 0b00000001000,
236240

237241
/** Whether this view has default change detection strategy (checks always) or onPush */
238-
CheckAlways = 0b000000100,
242+
CheckAlways = 0b00000010000,
239243

240244
/** Whether or not this view is currently dirty (needing check) */
241-
Dirty = 0b000001000,
245+
Dirty = 0b00000100000,
242246

243247
/** Whether or not this view is currently attached to change detection tree. */
244-
Attached = 0b000010000,
245-
246-
/**
247-
* Whether or not the init hooks have run.
248-
*
249-
* If on, the init hooks haven't yet been run and should be executed by the first component that
250-
* runs OR the first cR() instruction that runs (so inits are run for the top level view before
251-
* any embedded views).
252-
*/
253-
RunInit = 0b000100000,
248+
Attached = 0b00001000000,
254249

255250
/** Whether or not this view is destroyed. */
256-
Destroyed = 0b001000000,
251+
Destroyed = 0b00010000000,
257252

258253
/** Whether or not this view is the root view */
259-
IsRoot = 0b010000000,
254+
IsRoot = 0b00100000000,
255+
256+
/**
257+
* Index of the current init phase on last 23 bits
258+
*/
259+
IndexWithinInitPhaseIncrementer = 0b01000000000,
260+
IndexWithinInitPhaseShift = 9,
261+
IndexWithinInitPhaseReset = 0b00111111111,
262+
}
263+
264+
/**
265+
* Possible states of the init phase:
266+
* - 00: OnInit hooks to be run.
267+
* - 01: AfterContentInit hooks to be run
268+
* - 10: AfterViewInit hooks to be run
269+
* - 11: All init hooks have been run
270+
*/
271+
export const enum InitPhaseState {
272+
OnInitHooksToBeRun = 0b00,
273+
AfterContentInitHooksToBeRun = 0b01,
274+
AfterViewInitHooksToBeRun = 0b10,
275+
InitPhaseCompleted = 0b11,
260276
}
261277

262278
/**

packages/core/src/render3/state.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {executeHooks} from './hooks';
1111
import {ComponentDef, DirectiveDef} from './interfaces/definition';
1212
import {TElementNode, TNode, TNodeFlags, TViewNode} from './interfaces/node';
1313
import {LQueries} from './interfaces/query';
14-
import {BINDING_INDEX, CONTEXT, DECLARATION_VIEW, FLAGS, HOST_NODE, LView, LViewFlags, OpaqueViewState, QUERIES, TVIEW} from './interfaces/view';
14+
import {BINDING_INDEX, CONTEXT, DECLARATION_VIEW, FLAGS, HOST_NODE, InitPhaseState, LView, LViewFlags, OpaqueViewState, QUERIES, TVIEW} from './interfaces/view';
1515
import {isContentQueryHost} from './util';
1616

1717

@@ -336,11 +336,12 @@ export function leaveView(newView: LView): void {
336336
lView[FLAGS] &= ~LViewFlags.CreationMode;
337337
} else {
338338
try {
339-
executeHooks(lView, tView.viewHooks, tView.viewCheckHooks, checkNoChangesMode);
339+
executeHooks(
340+
lView, tView.viewHooks, tView.viewCheckHooks, checkNoChangesMode,
341+
InitPhaseState.AfterViewInitHooksToBeRun);
340342
} finally {
341343
// Views are clean and in update mode after being checked, so these bits are cleared
342344
lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass);
343-
lView[FLAGS] |= LViewFlags.RunInit;
344345
lView[BINDING_INDEX] = tView.bindingStartIndex;
345346
}
346347
}

0 commit comments

Comments
 (0)