/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {assertEqual} from '../util/assert'; import {DirectiveDef} from './interfaces/definition'; import {TNode} from './interfaces/node'; import {FLAGS, HookData, InitPhaseState, LView, LViewFlags, PREORDER_HOOK_FLAGS, PreOrderHookFlags, TView} from './interfaces/view'; /** * Adds all directive lifecycle hooks from the given `DirectiveDef` to the given `TView`. * * Must be run *only* on the first template pass. * * Sets up the pre-order hooks on the provided `tView`, * see {@link HookData} for details about the data structure. * * @param directiveIndex The index of the directive in LView * @param directiveDef The definition containing the hooks to setup in tView * @param tView The current TView * @param nodeIndex The index of the node to which the directive is attached * @param initialPreOrderHooksLength the number of pre-order hooks already registered before the * current process, used to know if the node index has to be added to the array. If it is -1, * the node index is never added. * @param initialPreOrderCheckHooksLength same as previous for pre-order check hooks */ export function registerPreOrderHooks( directiveIndex: number, directiveDef: DirectiveDef, tView: TView, nodeIndex: number, initialPreOrderHooksLength: number, initialPreOrderCheckHooksLength: number): void { ngDevMode && assertEqual(tView.firstTemplatePass, true, 'Should only be called on first template pass'); const {onChanges, onInit, doCheck} = directiveDef; if (initialPreOrderHooksLength >= 0 && (!tView.preOrderHooks || initialPreOrderHooksLength === tView.preOrderHooks.length) && (onChanges || onInit || doCheck)) { (tView.preOrderHooks || (tView.preOrderHooks = [])).push(nodeIndex); } if (initialPreOrderCheckHooksLength >= 0 && (!tView.preOrderCheckHooks || initialPreOrderCheckHooksLength === tView.preOrderCheckHooks.length) && (onChanges || doCheck)) { (tView.preOrderCheckHooks || (tView.preOrderCheckHooks = [])).push(nodeIndex); } if (onChanges) { (tView.preOrderHooks || (tView.preOrderHooks = [])).push(directiveIndex, onChanges); (tView.preOrderCheckHooks || (tView.preOrderCheckHooks = [])).push(directiveIndex, onChanges); } if (onInit) { (tView.preOrderHooks || (tView.preOrderHooks = [])).push(-directiveIndex, onInit); } if (doCheck) { (tView.preOrderHooks || (tView.preOrderHooks = [])).push(directiveIndex, doCheck); (tView.preOrderCheckHooks || (tView.preOrderCheckHooks = [])).push(directiveIndex, doCheck); } } /** * * Loops through the directives on the provided `tNode` and queues hooks to be * run that are not initialization hooks. * * Should be executed during `elementEnd()` and similar to * preserve hook execution order. Content, view, and destroy hooks for projected * components and directives must be called *before* their hosts. * * Sets up the content, view, and destroy hooks on the provided `tView`, * see {@link HookData} for details about the data structure. * * NOTE: This does not set up `onChanges`, `onInit` or `doCheck`, those are set up * separately at `elementStart`. * * @param tView The current TView * @param tNode The TNode whose directives are to be searched for hooks to queue */ export function registerPostOrderHooks(tView: TView, tNode: TNode): void { if (tView.firstTemplatePass) { // It's necessary to loop through the directives at elementEnd() (rather than processing in // directiveCreate) so we can preserve the current hook order. Content, view, and destroy // hooks for projected components and directives must be called *before* their hosts. for (let i = tNode.directiveStart, end = tNode.directiveEnd; i < end; i++) { const directiveDef = tView.data[i] as DirectiveDef; if (directiveDef.afterContentInit) { (tView.contentHooks || (tView.contentHooks = [])).push(-i, directiveDef.afterContentInit); } if (directiveDef.afterContentChecked) { (tView.contentHooks || (tView.contentHooks = [])).push(i, directiveDef.afterContentChecked); (tView.contentCheckHooks || (tView.contentCheckHooks = [ ])).push(i, directiveDef.afterContentChecked); } if (directiveDef.afterViewInit) { (tView.viewHooks || (tView.viewHooks = [])).push(-i, directiveDef.afterViewInit); } if (directiveDef.afterViewChecked) { (tView.viewHooks || (tView.viewHooks = [])).push(i, directiveDef.afterViewChecked); (tView.viewCheckHooks || (tView.viewCheckHooks = [ ])).push(i, directiveDef.afterViewChecked); } if (directiveDef.onDestroy != null) { (tView.destroyHooks || (tView.destroyHooks = [])).push(i, directiveDef.onDestroy); } } } } /** * Executing hooks requires complex logic as we need to deal with 2 constraints. * * 1. Init hooks (ngOnInit, ngAfterContentInit, ngAfterViewInit) must all be executed once and only * once, across many change detection cycles. This must be true even if some hooks throw, or if * some recursively trigger a change detection cycle. * To solve that, it is required to track the state of the execution of these init hooks. * This is done by storing and maintaining flags in the view: the {@link InitPhaseState}, * and the index within that phase. They can be seen as a cursor in the following structure: * [[onInit1, onInit2], [afterContentInit1], [afterViewInit1, afterViewInit2, afterViewInit3]] * They are are stored as flags in LView[FLAGS]. * * 2. Pre-order hooks can be executed in batches, because of the select instruction. * To be able to pause and resume their execution, we also need some state about the hook's array * that is being processed: * - the index of the next hook to be executed * - the number of init hooks already found in the processed part of the array * They are are stored as flags in LView[PREORDER_HOOK_FLAGS]. */ /** * Executes necessary hooks at the start of executing a template. * * Executes hooks that are to be run during the initialization of a directive such * as `onChanges`, `onInit`, and `doCheck`. * * @param lView The current view * @param tView Static data for the view containing the hooks to be executed * @param checkNoChangesMode Whether or not we're in checkNoChanges mode. * @param @param currentNodeIndex 2 cases depending the the value: * - undefined: execute hooks only from the saved index until the end of the array (pre-order case, * when flushing the remaining hooks) * - number: execute hooks only from the saved index until that node index exclusive (pre-order * case, when executing select(number)) */ export function executePreOrderHooks( currentView: LView, tView: TView, checkNoChangesMode: boolean, currentNodeIndex: number | undefined): void { if (!checkNoChangesMode) { executeHooks( currentView, tView.preOrderHooks, tView.preOrderCheckHooks, checkNoChangesMode, InitPhaseState.OnInitHooksToBeRun, currentNodeIndex !== undefined ? currentNodeIndex : null); } } /** * Executes hooks against the given `LView` based off of whether or not * This is the first pass. * * @param currentView The view instance data to run the hooks against * @param firstPassHooks An array of hooks to run if we're in the first view pass * @param checkHooks An Array of hooks to run if we're not in the first view pass. * @param checkNoChangesMode Whether or not we're in no changes mode. * @param initPhaseState the current state of the init phase * @param currentNodeIndex 3 cases depending the the value: * - undefined: all hooks from the array should be executed (post-order case) * - null: execute hooks only from the saved index until the end of the array (pre-order case, when * flushing the remaining hooks) * - number: execute hooks only from the saved index until that node index exclusive (pre-order * case, when executing select(number)) */ export function executeHooks( currentView: LView, firstPassHooks: HookData | null, checkHooks: HookData | null, checkNoChangesMode: boolean, initPhaseState: InitPhaseState, currentNodeIndex: number | null | undefined): void { if (checkNoChangesMode) return; const hooksToCall = (currentView[FLAGS] & LViewFlags.InitPhaseStateMask) === initPhaseState ? firstPassHooks : checkHooks; if (hooksToCall) { callHooks(currentView, hooksToCall, initPhaseState, currentNodeIndex); } // The init phase state must be always checked here as it may have been recursively updated if (currentNodeIndex == null && (currentView[FLAGS] & LViewFlags.InitPhaseStateMask) === initPhaseState && initPhaseState !== InitPhaseState.InitPhaseCompleted) { currentView[FLAGS] &= LViewFlags.IndexWithinInitPhaseReset; currentView[FLAGS] += LViewFlags.InitPhaseStateIncrementer; } } /** * Calls lifecycle hooks with their contexts, skipping init hooks if it's not * the first LView pass * * @param currentView The current view * @param arr The array in which the hooks are found * @param initPhaseState the current state of the init phase * @param currentNodeIndex 3 cases depending the the value: * - undefined: all hooks from the array should be executed (post-order case) * - null: execute hooks only from the saved index until the end of the array (pre-order case, when * flushing the remaining hooks) * - number: execute hooks only from the saved index until that node index exclusive (pre-order * case, when executing select(number)) */ function callHooks( currentView: LView, arr: HookData, initPhase: InitPhaseState, currentNodeIndex: number | null | undefined): void { const startIndex = currentNodeIndex !== undefined ? (currentView[PREORDER_HOOK_FLAGS] & PreOrderHookFlags.IndexOfTheNextPreOrderHookMaskMask) : 0; const nodeIndexLimit = currentNodeIndex != null ? currentNodeIndex : -1; let lastNodeIndexFound = 0; for (let i = startIndex; i < arr.length; i++) { const hook = arr[i + 1] as() => void; if (typeof hook === 'number') { lastNodeIndexFound = arr[i] as number; if (currentNodeIndex != null && lastNodeIndexFound >= currentNodeIndex) { break; } } else { const isInitHook = arr[i] < 0; if (isInitHook) currentView[PREORDER_HOOK_FLAGS] += PreOrderHookFlags.NumberOfInitHooksCalledIncrementer; if (lastNodeIndexFound < nodeIndexLimit || nodeIndexLimit == -1) { callHook(currentView, initPhase, arr, i); currentView[PREORDER_HOOK_FLAGS] = (currentView[PREORDER_HOOK_FLAGS] & PreOrderHookFlags.NumberOfInitHooksCalledMask) + i + 2; } i++; } } } /** * Execute one hook against the current `LView`. * * @param currentView The current view * @param initPhaseState the current state of the init phase * @param arr The array in which the hooks are found * @param i The current index within the hook data array */ function callHook(currentView: LView, initPhase: InitPhaseState, arr: HookData, i: number) { const isInitHook = arr[i] < 0; const hook = arr[i + 1] as() => void; const directiveIndex = isInitHook ? -arr[i] : arr[i] as number; const directive = currentView[directiveIndex]; if (isInitHook) { const indexWithintInitPhase = currentView[FLAGS] >> LViewFlags.IndexWithinInitPhaseShift; // The init phase state must be always checked here as it may have been recursively // updated if (indexWithintInitPhase < (currentView[PREORDER_HOOK_FLAGS] >> PreOrderHookFlags.NumberOfInitHooksCalledShift) && (currentView[FLAGS] & LViewFlags.InitPhaseStateMask) === initPhase) { currentView[FLAGS] += LViewFlags.IndexWithinInitPhaseIncrementer; hook.call(directive); } } else { hook.call(directive); } }