fix(ivy): support string tokens in dependency injection (#27383)

In Angular, it used to be an accepted practice to use strings as dependency
injection tokens. E.g. {provide: 'test', useValue: 'provided'}. However,
the Ivy node injection system did not support this. The Ivy DI system
attempts to patch a Bloom bit index onto each type registered with it, and
this patch operation does not work for a string token.

This commit adds string token support to the bloom filter system by
reserving bit 0 for string tokens. This eliminates the need for each string
token to store its own Bloom bit, at the expense of slightly more expensive
lookups of string tokens.

PR Close #27383
This commit is contained in:
Alex Rickabaugh 2018-11-28 12:51:00 -08:00 committed by Igor Minar
parent c96dea27ce
commit 64a34616d8
2 changed files with 39 additions and 5 deletions

View File

@ -87,9 +87,10 @@ let nextNgElementId = 0;
* @param type The directive token to register * @param type The directive token to register
*/ */
export function bloomAdd( export function bloomAdd(
injectorIndex: number, tView: TView, type: Type<any>| InjectionToken<any>): void { injectorIndex: number, tView: TView, type: Type<any>| InjectionToken<any>| string): void {
ngDevMode && assertEqual(tView.firstTemplatePass, true, 'expected firstTemplatePass to be true'); ngDevMode && assertEqual(tView.firstTemplatePass, true, 'expected firstTemplatePass to be true');
let id: number|undefined = (type as any)[NG_ELEMENT_ID]; let id: number|undefined =
typeof type !== 'string' ? (type as any)[NG_ELEMENT_ID] : type.charCodeAt(0) || 0;
// Set a unique ID on the directive type, so if something tries to inject the directive, // Set a unique ID on the directive type, so if something tries to inject the directive,
// we can easily retrieve the ID and hash it into the bloom bit that should be checked. // we can easily retrieve the ID and hash it into the bloom bit that should be checked.
@ -502,9 +503,12 @@ export function getNodeInjectable(
* @param token the injection token * @param token the injection token
* @returns the matching bit to check in the bloom filter or `null` if the token is not known. * @returns the matching bit to check in the bloom filter or `null` if the token is not known.
*/ */
export function bloomHashBitOrFactory(token: Type<any>| InjectionToken<any>): number|Function| export function bloomHashBitOrFactory(token: Type<any>| InjectionToken<any>| string): number|
undefined { Function|undefined {
ngDevMode && assertDefined(token, 'token must be defined'); ngDevMode && assertDefined(token, 'token must be defined');
if (typeof token === 'string') {
return token.charCodeAt(0) || 0;
}
const tokenId: number|undefined = (token as any)[NG_ELEMENT_ID]; const tokenId: number|undefined = (token as any)[NG_ELEMENT_ID];
return typeof tokenId === 'number' ? tokenId & BLOOM_MASK : tokenId; return typeof tokenId === 'number' ? tokenId & BLOOM_MASK : tokenId;
} }

View File

@ -11,7 +11,7 @@ import {RenderFlags} from '@angular/core/src/render3/interfaces/definition';
import {defineComponent} from '../../src/render3/definition'; import {defineComponent} from '../../src/render3/definition';
import {bloomAdd, bloomHasToken, bloomHashBitOrFactory as bloomHash, getOrCreateNodeInjectorForNode} from '../../src/render3/di'; import {bloomAdd, bloomHasToken, bloomHashBitOrFactory as bloomHash, getOrCreateNodeInjectorForNode} from '../../src/render3/di';
import {defineDirective, elementProperty, load, templateRefExtractor} from '../../src/render3/index'; import {ProvidersFeature, defineDirective, elementProperty, load, templateRefExtractor} from '../../src/render3/index';
import {bind, container, containerRefreshEnd, containerRefreshStart, createNodeAtIndex, createLView, createTView, directiveInject, element, elementEnd, elementStart, embeddedViewEnd, embeddedViewStart, injectAttribute, interpolation2, projection, projectionDef, reference, template, text, textBinding, elementContainerStart, elementContainerEnd} from '../../src/render3/instructions'; import {bind, container, containerRefreshEnd, containerRefreshStart, createNodeAtIndex, createLView, createTView, directiveInject, element, elementEnd, elementStart, embeddedViewEnd, embeddedViewStart, injectAttribute, interpolation2, projection, projectionDef, reference, template, text, textBinding, elementContainerStart, elementContainerEnd} from '../../src/render3/instructions';
import {isProceduralRenderer, RElement} from '../../src/render3/interfaces/renderer'; import {isProceduralRenderer, RElement} from '../../src/render3/interfaces/renderer';
@ -1719,6 +1719,36 @@ describe('di', () => {
}); });
}); });
describe('string tokens', () => {
it('should be able to provide a string token', () => {
let injectorDir !: InjectorDir;
let divElement !: HTMLElement;
class InjectorDir {
constructor(public value: string) {}
static ngDirectiveDef = defineDirective({
type: InjectorDir,
selectors: [['', 'injectorDir', '']],
factory: () => injectorDir = new InjectorDir(directiveInject('test' as any)),
features: [ProvidersFeature([{provide: 'test', useValue: 'provided'}])],
});
}
/** <div injectorDir otherInjectorDir></div> */
const App = createComponent('app', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
element(0, 'div', ['injectorDir', '']);
}
// testing only
divElement = load(0);
}, 1, 0, [InjectorDir]);
const fixture = new ComponentFixture(App);
expect(injectorDir.value).toBe('provided');
});
});
describe('Renderer2', () => { describe('Renderer2', () => {
class MyComp { class MyComp {
constructor(public renderer: Renderer2) {} constructor(public renderer: Renderer2) {}