diff --git a/packages/core/src/render3/assert.ts b/packages/core/src/render3/assert.ts index 15f8776f67..0c30b7cec9 100644 --- a/packages/core/src/render3/assert.ts +++ b/packages/core/src/render3/assert.ts @@ -6,11 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -/** - * The functions in this file verify that the assumptions we are making - * about state in an instruction are correct before implementing any logic. - * They are meant only to be called in dev mode as sanity checks. - */ +// The functions in this file verify that the assumptions we are making +// about state in an instruction are correct before implementing any logic. +// They are meant only to be called in dev mode as sanity checks. /** * Stringifies values such that strings are wrapped in explicit quotation marks and @@ -19,8 +17,8 @@ * * e.g. `expected "3" to be 3` is easier to understand than `expected 3 to be 3`. * - * @param {any} value - The value to be stringified - * @returns {string} the stringified value + * @param value The value to be stringified + * @returns The stringified value */ function stringifyValueForError(value: any): string { return typeof value === 'string' ? `"${value}"` : '' + value; @@ -50,11 +48,11 @@ export function assertNotEqual(actual: T, expected: T, name: string) { /** * Throws an error with a message constructed from the arguments. * - * @param {T} actual - The actual value (e.g. 3) - * @param {T} expected - The expected value (e.g. 5) - * @param {string} name - The name of the value being checked (e.g. attrs.length) - * @param {string} operator - The comparison operator (e.g. <, >, ==) - * @param {(v: T) => string)} serializer - Function that maps a value to its display value + * @param actual The actual value (e.g. 3) + * @param expected The expected value (e.g. 5) + * @param name The name of the value being checked (e.g. attrs.length) + * @param operator The comparison operator (e.g. <, >, ==) + * @param serializer Function that maps a value to its display value */ export function assertThrow( actual: T, expected: T, name: string, operator: string, diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index 693a8bdbf3..036e778938 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -14,20 +14,16 @@ import {LContainer, LNodeFlags, LNodeInjector} from './interfaces'; import {ComponentTemplate, DirectiveDef} from './public_interfaces'; import {stringify, notImplemented} from './util'; -/** - * Injection flags for DI. - * - * Optional: The dependency is not required. - * - * CheckSelf: Should check the current node for the dependency. - * - * CheckParent: Should check parent nodes for the dependency. - */ +/** Injection flags for DI. */ export const enum InjectFlags { + /** Dependency is not required. Null will be injected if there is no provider for the dependency. */ Optional = 1 << 0, + /** When resolving a dependency, include the node that is requesting injection. */ CheckSelf = 1 << 1, + /** When resolving a dependency, include ancestors of the node requesting injection. */ CheckParent = 1 << 2, - Default = CheckSelf | CheckParent + /** Default injection options: required, checks both self and ancestors. */ + Default = CheckSelf | CheckParent, } /** @@ -53,6 +49,17 @@ function createInjectionError(text: string, token: any) { * If not found, it will propagate up to the next parent injector until the token * is found or the top is reached. * + * Usage example (in factory function): + * + * class SomeDirective { + * constructor(directive: DirectiveA) {} + * + * static ngDirectiveDef = defineDirective({ + * type: SomeDirective, + * factory: () => new SomeDirective(inject(DirectiveA)) + * }); + * } + * * @param token The directive type to search for * @param flags Injection flags (e.g. CheckParent) * @returns The instance found @@ -60,6 +67,9 @@ function createInjectionError(text: string, token: any) { export function inject(token: viewEngine.Type, flags?: InjectFlags): T { const di = getOrCreateNodeInjector(); const bloomHash = bloomHashBit(token); + + // If the token has a bloom hash, then it is a directive that is public to the injection system + // (diPublic). If there is no hash, fall back to the module injector. if (bloomHash === null) { const moduleInjector = di.injector; if (!moduleInjector) { @@ -68,27 +78,49 @@ export function inject(token: viewEngine.Type, flags?: InjectFlags): T { moduleInjector.get(token); } else { let injector: LNodeInjector|null = di; + while (injector) { + // Get the closest potential matching injector (upwards in the injector tree) that + // *potentially* has the token. injector = bloomFindPossibleInjector(injector, bloomHash); - if (injector) { - const node = injector.node; - const flags = node.flags; - let size = flags & LNodeFlags.SIZE_MASK; - if (size !== 0) { - size = size >> LNodeFlags.SIZE_SHIFT; - const start = flags >> LNodeFlags.INDX_SHIFT; - const ngStaticData = node.view.ngStaticData; - for (let i = start, ii = start + size; i < ii; i++) { - const def = ngStaticData[i] as DirectiveDef; - if (def.diPublic && def.type == token) { - return node.view.data[i]; - } + + // If no injector is found, we *know* that there is no ancestor injector that contains the + // token, so we abort. + if (!injector) { break; } + + // At this point, we have an injector which *may* contain the token, so we step through the + // directives associated with the injector's corresponding node to get the directive instance. + const node = injector.node; + + // The size of the node's directive's list is stored in certain bits of the node's flags, + // so exact it with a mask and shift it back such that the bits reflect the real value. + const flags = node.flags; + const size = (flags & LNodeFlags.SIZE_MASK) >> LNodeFlags.SIZE_SHIFT; + + if (size !== 0) { + // The start index of the directives list is also part of the node's flags, but there is + // nothing to the "left" of it so it doesn't need a mask. + const start = flags >> LNodeFlags.INDX_SHIFT; + + const ngStaticData = node.view.ngStaticData; + for (let i = start, ii = start + size; i < ii; i++) { + // Get the definition for the directive at this index and, if it is injectable (diPublic), + // and matches the given token, return the directive instance. + const directiveDef = ngStaticData[i] as DirectiveDef; + if (directiveDef.diPublic && directiveDef.type == token) { + return node.view.data[i]; } } - injector = injector.parent; } + + // If we *didn't* find the directive for the token from the candidate injector, we had a false + // positive. Traverse up the tree and continue. + injector = injector.parent; } } + + // No directive was found for the given token. + // TODO: implement optional, check-self, and check-parent. throw createInjectionError('Not found', token); } @@ -113,7 +145,7 @@ function bloomHashBit(type: viewEngine.Type): number|null { * Finds the closest injector that might have a certain directive. * * Each directive corresponds to a bit in an injector's bloom filter. Given the bloom bit to - * check and a starting injector, this function propagates up injectors until it finds an + * check and a starting injector, this function traverses up injectors until it finds an * injector that contains a 1 for that bit in its bloom filter. A 1 indicates that the * injector may have that directive. It only *may* have the directive because directives begin * to share bloom filter bits after the BLOOM_SIZE is reached, and it could correspond to a @@ -128,22 +160,36 @@ function bloomHashBit(type: viewEngine.Type): number|null { * @param bloomBit The bit to check in each injector's bloom filter * @returns An injector that might have the directive */ -export function bloomFindPossibleInjector(injector: LNodeInjector, bloomBit: number): LNodeInjector| - null { +export function bloomFindPossibleInjector(startInjector: LNodeInjector, bloomBit: number): LNodeInjector| + null { + // Create a mask that targets the specific bit associated with the directive we're looking for. + // This will be a number between 0 and 31, corresponding to a bit position in a 32 bit integer. const mask = 1 << bloomBit; - let di: LNodeInjector|null = injector; - while (di) { - // See if the current injector may have the value. + + // Traverse up the injector tree until we find a potential match or until we know there *isn't* a + // match. + let injector: LNodeInjector|null = startInjector; + while (injector) { + // Our bloom filter size is 128 bits, which is four 32-bit bloom filter buckets: + // bf0 = [0 - 31], bf1 = [32 - 63], bf2 = [64 - 95], bf3 = [96 - 127] + // Get the bloom filter value from the appropriate bucket based on the directive's bloomBit. let value: number = - bloomBit < 64 ? (bloomBit < 32 ? di.bf0 : di.bf1) : (bloomBit < 96 ? di.bf2 : di.bf3); + bloomBit < 64 ? (bloomBit < 32 ? injector.bf0 : injector.bf1) : (bloomBit < 96 ? injector.bf2 : injector.bf3); + + // If the bloom filter value has the bit corresponding to the directive's bloomBit flipped on, + // this injector is a potential match. if ((value & mask) === mask) { - return di; + return injector; } - // See if the parent injectors may have the value + + // If the current injector does not have the directive, check the bloom filters for the ancestor + // injectors (cbf0 - cbf3). These filters capture *all* ancestor injectors. value = - bloomBit < 64 ? (bloomBit < 32 ? di.cbf0 : di.cbf1) : (bloomBit < 96 ? di.cbf2 : di.cbf3); - // Only go to parent if parent may have value otherwise exit. - di = (value & mask) ? di.parent : null; + bloomBit < 64 ? (bloomBit < 32 ? injector.cbf0 : injector.cbf1) : (bloomBit < 96 ? injector.cbf2 : injector.cbf3); + + // If the ancestor bloom filter value has the bit corresponding to the directive, traverse up to + // find the specific injector. If the ancestor bloom filter does not have the bit, we can abort. + injector = (value & mask) ? injector.parent : null; } return null; } diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 89fdab46fe..3c97c31061 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -17,8 +17,8 @@ import {RComment, RElement, RNode, RText, Renderer3Fn} from './renderer'; * This is necessary to add or remove elements from the DOM when a view * is added or removed from the container. e.g. parent.removeChild(...) * - * @param {LContainer} containerNode The container node whose parent must be found - * @returns {RNode} + * @param containerNode The container node whose parent must be found + * @returns Closest DOM node above the container */ export function findNativeParent(containerNode: LContainer): RNode|null { let container: LContainer|null = containerNode; @@ -49,10 +49,10 @@ export function findNativeParent(containerNode: LContainer): RNode|null { * the next view's first child element. Otherwise, the container's comment * anchor is the marker. * - * @param {number} index The index of the view to check - * @param {ContainerState} state ContainerState of the parent container - * @param {RComment} native Comment anchor for container - * @returns {RElement | RText | RComment} + * @param index The index of the view to check + * @param state ContainerState of the parent container + * @param native Comment anchor for container + * @returns The DOM element for which the view should insert elements */ export function findBeforeNode(index: number, state: ContainerState, native: RComment): RElement| RText|RComment { @@ -70,10 +70,10 @@ export function findBeforeNode(index: number, state: ContainerState, native: RCo * to propagate deeply into the nested containers to remove all elements in the * views beneath it. * - * @param {LContainer} container - The container to which the root view belongs - * @param {LView} rootNode - The view from which elements should be added or removed - * @param {boolean} insertMode - Whether or not elements should be added (if false, removing) - * @param {RNode} beforeNode - The node before which elements should be added, if insert mode + * @param container The container to which the root view belongs + * @param rootNode The view from which elements should be added or removed + * @param insertMode Whether or not elements should be added (if false, removing) + * @param beforeNode The node before which elements should be added, if insert mode */ export function addRemoveViewFromContainer( container: LContainer, rootNode: LView, insertMode: true, beforeNode: RNode | null): void; @@ -142,7 +142,7 @@ export function addRemoveViewFromContainer( * - Using a while loop because it's faster than recursion * - Destroy only called on movement to sibling or movement to parent (laterally or up) * - * @param {ViewState} rootView - The view to destroy + * @param rootView The view to destroy */ export function destroyViewTree(rootView: ViewState): void { let viewOrContainerState: ViewOrContainerState|null = rootView; @@ -180,10 +180,10 @@ export function destroyViewTree(rootView: ViewState): void { * root node of another view (in that case, the view's elements will be added when * the container's parent view is added later). * - * @param {LContainer} container - The container into which the view should be inserted - * @param {LView} newView - The view to insert - * @param {number} index - The index at which to insert the view - * @returns {LView} - The inserted view + * @param container The container into which the view should be inserted + * @param newView The view to insert + * @param index The index at which to insert the view + * @returns The inserted view */ export function insertView(container: LContainer, newView: LView, index: number): LView { const state = container.data; @@ -226,9 +226,9 @@ export function insertView(container: LContainer, newView: LView, index: number) * removes the view's elements from the DOM and conducts cleanup (e.g. removing * listeners, calling onDestroys). * - * @param {LContainer} container - The container from which to remove a view - * @param {number} removeIndex - The index of the view to remove - * @returns {LView} - The removed view + * @param container The container from which to remove a view + * @param removeIndex The index of the view to remove + * @returns The removed view */ export function removeView(container: LContainer, removeIndex: number): LView { const children = container.data.children; @@ -249,8 +249,8 @@ export function removeView(container: LContainer, removeIndex: number): LView { * one view to the next to add/remove elements. Also adds the ViewState (view.data) * to the view tree for easy traversal when cleaning up the view. * - * @param {LView} view - The view to set up - * @param {LView} next - The view's new next + * @param view The view to set up + * @param next The view's new next */ export function setViewNext(view: LView, next: LView | null): void { view.next = next; @@ -265,9 +265,9 @@ export function setViewNext(view: LView, next: LView | null): void { * embedded views, the container (which is the view node's parent, but not the * ViewState's parent) needs to be checked for a possible next property. * - * @param {ViewOrContainerState} state - The ViewOrContainerState for which we need a parent state - * @param {ViewState} rootView - The rootView, so we don't propagate too far up the view tree - * @returns {ViewOrContainerState} + * @param state The ViewOrContainerState for which we need a parent state + * @param rootView The rootView, so we don't propagate too far up the view tree + * @returns The correct parent ViewOrContainerState */ export function getParentState( state: ViewOrContainerState, rootView: ViewState): ViewOrContainerState|null { @@ -286,7 +286,7 @@ export function getParentState( /** * Removes all listeners and call all onDestroys in a given view. * - * @param {ViewState} viewState - The ViewState of the view to clean up + * @param viewState The ViewState of the view to clean up */ function cleanUpView(viewState: ViewState): void { if (!viewState.cleanup) return; @@ -310,10 +310,10 @@ function cleanUpView(viewState: ViewState): void { * of a parent component, the child will be appended to the right position later by * the content projection system. Otherwise, append normally. * - * @param {LNode} parent - The parent to which to append the child - * @param {RNode} child - The child that should be appended - * @param {ViewState} currentView - The current view's ViewState - * @returns {boolean} - Whether or not the child was appended + * @param parent The parent to which to append the child + * @param child The child that should be appended + * @param currentView The current view's ViewState + * @returns Whether or not the child was appended */ export function appendChild(parent: LNode, child: RNode | null, currentView: ViewState): boolean { // Only add native child element to parent element if the parent element is regular Element. @@ -347,8 +347,8 @@ export function appendChild(parent: LNode, child: RNode | null, currentView: Vie * of a parent component, the child will be inserted to the right position later by * the content projection system. Otherwise, insertBefore normally. * - * @param {LNode} node - Node to insert - * @param {ViewState} currentView - The current view's ViewState + * @param node Node to insert + * @param currentView The current view's ViewState */ export function insertChild(node: LNode, currentView: ViewState): void { const parent = node.parent !; @@ -383,10 +383,10 @@ export function insertChild(node: LNode, currentView: ViewState): void { * appends the nodes from all of the container's active views to the DOM. Also stores the * node in the given projectedNodes array. * - * @param {ProjectionState} projectedNodes - Array to store the projected node - * @param {LElement | LText | LContainer} node - The node to process - * @param {LView | LElement} currentParent - The last parent element to be processed - * @param {ViewState} currentView - The current view's ViewState + * @param projectedNodes Array to store the projected node + * @param node The node to process + * @param currentParent The last parent element to be processed + * @param currentView The current view's ViewState */ export function processProjectedNode( projectedNodes: ProjectionState, node: LElement | LText | LContainer,