From 86a3be8610eda6e0c497ac0ca6dd73e8a296cca4 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Fri, 31 Aug 2018 13:35:15 -0700 Subject: [PATCH] docs(ivy): add explanation of LViewData (#25779) PR Close #25779 --- packages/core/src/render3/VIEW_DATA.md | 445 +++++++++++++++++++++++++ 1 file changed, 445 insertions(+) create mode 100644 packages/core/src/render3/VIEW_DATA.md diff --git a/packages/core/src/render3/VIEW_DATA.md b/packages/core/src/render3/VIEW_DATA.md new file mode 100644 index 0000000000..01dc9461fc --- /dev/null +++ b/packages/core/src/render3/VIEW_DATA.md @@ -0,0 +1,445 @@ +# View Data Explanation + +`LViewData` and `TView.data` are how the Ivy renderer keeps track of the internal data needed to render the template. +`LViewData` is designed so that a single array can contain all of the necessary data for the template rendering in a compact form. +`TView.data` is a corollary to the `LViewData` and contains information which can be shared across the template instances. + +## `LViewData` / `TView.data` layout. + +Both `LViewData` and `TView.data` are arrays whose indices refer to the same item. +For example index `123` may point to a component instance in the `LViewData` but a component type in `TView.data`. + +The layout is as such: + +| Section | `LViewData` | `TView.data` +| ---------- | --------------------------------------------- | -------------------------------------------------- +| `HEADER` | contextual data | mostly `null` +| `CONSTS` | DOM, pipe, and local ref instances | +| `VARS` | binding values | property names +| `EXPANDO` | host bindings; directive instances; providers | host prop names; directive tokens; provider tokens + + +## `HEADER` + +`HEADER` is a fixed array size which contains contextual information about the template. +Mostly information such as parent `LViewData`, `Sanitizer`, `TView`, and many more bits of information needed for template rendering. + + +## `CONSTS` + +`CONSTS` contain the DOM elements, pipe instances, and local refs. +The size of the `CONSTS` section is declared in the property `consts` of the component definition. + +```typescript +@Component({ + template: `
Hello World!
` +}) +class MyApp { + + static ngComponentDef = defineComponent({ + ..., + consts: 5, + template: function(rf: RenderFlags, ctx: MyApp) { + if (rf & RenderFlags.Create) { + elementStart(0, 'div'); + text(1, 'Hello '); + elementStart(2, 'b'); + text(3, 'World'); + elementEnd(); + text(4, '!'); + elementEnd(); + } + ... + } + }); +} +``` + +The above will create following layout: + +| Index | `LViewData` | `TView.data` +| ----: | ----------- | ------------ +| `HEADER` +| `CONSTS` +| 10 | `
` | `{type: Element, index: 10, parent: null}` +| 11 | `#text(Hello )` | `{type: Element, index: 11, parent: tView.data[10]}` +| 12 | `` | `{type: Element, index: 12, parent: tView.data[10]}` +| 13 | `#text(World)` | `{type: Element, index: 13, parent: tView.data[12]}` +| 14 | `#text(!)` | `{type: Element, index: 14, parent: tView.data[10]}` +| ... | ... | ... + +NOTE: +- The `10` is not the actual size of `HEADER` but it is left here for simplification. +- `LViewData` contains DOM instances only +- `TView.data` contains information on relationships such as where the parent is. + You need the `TView.data` information to make sense of the `LViewData` information. + + +## `VARS` + +`VARS` contains information on how to process the bindings. +The size of the `VARS `section is declared in the property `vars` of the component definition. + +```typescript +@Component({ + template: `
Hello {{name}}!
` +}) +class MyApp { + name = 'World'; + + static ngComponentDef = defineComponent({ + ..., + consts: 2, // Two DOM Elements. + vars: 2, // Two bindings. + template: function(rf: RenderFlags, ctx: MyApp) { + if (rf & RenderFlags.Create) { + elementStart(0, 'div'); + text(1); + elementEnd(); + } + if (rf & RenderFlags.Update) { + elementProperty(0, 'title', bind(ctx.name)); + textBinding(1, interpolation1('Hello ', ctx.name, '!')); + } + ... + } + }); +} +``` + +The above will create following layout: + +| Index | `LViewData` | `TView.data` +| ----: | ----------- | ------------ +| `HEADER` +| `CONSTS` +| 10 | `
` | `{type: Element, index: 10, parent: null}` +| 11 | `#text()` | `{type: Element, index: 11, parent: tView.data[10]}` +| `VARS` +| 12 | `'World'` | `'title'` +| 13 | `'World'` | `null` +| ... | ... | ... + +NOTE: +- `LViewData` contain DOM instances and previous binding values only +- `TView.data` contains information on relationships and property labels. + + + +## `EXPANDO` + +*TODO*: This section is to be implemented. + +`EXPANDO` contains information on data which size is not known at compile time. +Examples include: +- `Component`/`Directives` since we don't know at compile time which directives will match. +- Host bindings, since until we match the directives it is unclear how many host bindings need to be allocated. + +```typescript +@Component({ + template: `` +}) +class MyApp { + + static ngComponentDef = defineComponent({ + ..., + consts: 1, + template: function(rf: RenderFlags, ctx: MyApp) { + if (rf & RenderFlags.Create) { + element(0, 'child', ['tooltip', null]); + } + ... + }, + directives: [Child, Tooltip] + }); +} + + +@Component({ + selector: 'child', + ... +}) +class Child { + @HostBinding('tooltip') hostTitle = 'Hello World!'; + static ngComponentDef = defineComponent({ + ... + hostVars: 1 + }); + ... +} + +@Directive({ + selector: '[tooltip]' +}) +class Tooltip { + @HostBinding('title') hostTitle = 'greeting'; + static ngDirectiveDef = defineDirective({ + ... + hostVars: 1 + }); + ... +} +``` + + +The above will create the following layout: + +| Index | `LViewData` | `TView.data` +| ----: | ----------- | ------------ +| `HEADER` +| `CONSTS` +| 10 | `[, ...]` | `{type: Element, index: 10, parent: null}` +| `VARS` +| `EXPANDO` +| 11..18| cumulativeBloom | templateBloom +| 19 | `new Child()` | `Child` +| 20 | `new Tooltip()` | `Tooltip` +| 21 | `'Hello World!'` | `'tooltip'` +| 22 | `'greeting'` | `'title'` +| ... | ... | ... + + +The `EXPANDO` section needs additional information for information stored in `TView.expandoInstructions` + +| Index | `TView.expandoInstructions` | Meaning +| ----: | ---------------------------: | ------- +| 0 | -10 | Negative numbers signifies pointers to elements. In this case 10 (``) +| 1 | 2 | Injector size. Number of values to skip to get to Host Bindings. +| 2 | Child.ngComponentDef.hostBindings | The function to call. (Only when `hostVars` is not `0`) +| 3 | Child.ngComponentDef.hostVars | Number of host bindings to process. (Only when `hostVars` is not `0`) +| 4 | Tooltip.ngDirectiveDef.hostBindings | The function to call. (Only when `hostVars` is not `0`) +| 5 | Tooltip.ngDirectiveDef.hostVars | Number of host bindings to process. (Only when `hostVars` is not `0`) + +The reason for this layout is to make the host binding update efficient using this pseudo code: +```typescript +let currentDirectiveIndex = -1; +let currentElementIndex = -1; +// This is global state which is used internally by hostBindings to know where the offset is +let bindingRootIndex = tView.expandoStart; +for(var i = 0; i < tview.expandoInstructions.length; i++) { + let instruction = tview.expandoInstructions[i]; + if (typeof instruction === 'number') { + // Numbers are used to update the indices. + if (instruction < 0) { + // Negative numbers means that we are starting new EXPANDO block and need to update current element and directive index + bindingRootIndex += BLOOM_OFFSET; + currentDirectiveIndex = bindingRootIndex; + currentElementIndex = -instruction; + } else { + bindingRootIndex += instruction; + } + } else { + // We know that we are hostBinding function so execute it. + instruction(currentDirectiveIndex, currentElementIndex); + currentDirectiveIndex++; + } +} +``` + +The above code should execute as: + +| Instruction | `bindingRootIndex` | `currentDirectiveIndex` | `currentElementIndex` +| ----------: | -----------------: | ----------------------: | --------------------: +| (initial) | `11` | `-1` | `-1` +| `-10` | `19` | `\* new Child() *\ 19` | `\* *\ 10` +| `2` | `21` | `\* new Child() *\ 19` | `\* *\ 10` +| `Child.ngComponentDef.hostBindings` | invoke with => | `\* new Child() *\ 19` | `\* *\ 10` +| | `21` | `\* new Tooltip() *\ 20` | `\* *\ 10` +| `Child.ngComponentDef.hostVars` | `22` | `\* new Tooltip() *\ 20` | `\* *\ 10` +| `Tooltip.ngDirectiveDef.hostBindings` | invoke with => | `\* new Tooltip() *\ 20` | `\* *\ 10` +| | `22` | `21` | `\* *\ 10` +| `Tooltip.ngDirectiveDef.hostVars` | `22` | `21` | `\* *\ 10` + +## `EXPANDO` and Injection + +`EXPANDO` will also store the injection information for the element. +This is because at the time of compilation we don't know about all of the injection tokens which will need to be created. +(The injection tokens are part of the Component hence hide behind a selector and are not available to the parent component.) + +Injection needs to store three things: +- The injection token stored in `TView.data` +- The token factory stored in `LProtoViewData` and subsequently in `LViewData` +- The value for the injection token stored in `LViewData`. (Replacing token factory upon creation). + +To save time when creating `LViewData` we use an array clone operation to copy data from `LProtoViewdata` to `LViewData`. +The `LProtoViewData` is initialized by the `ProvidesFeature`. + +Injection tokens are sorted into three sections: +1. `directives`: Used to denote eagerly created items representing directives and component. +2. `providers`: Used to denote items visible to component, component's view and component's content. +3. `viewProviders`: Used to denote items only visible to the component's view. + +```typescript +@Component({ + template: `` +}) +class MyApp { + + static ngComponentDef = defineComponent({ + ..., + consts: 1, + template: function(rf: RenderFlags, ctx: MyApp) { + if (rf & RenderFlags.Create) { + element(0, 'child'); + } + ... + }, + directives: [Child] + }); +} + + +@Component({ + selector: 'child', + providers: [ + ServiceA, + {provide: ServiceB, useValue: 'someServiceBValue'}, + ], + viewProviders: [ + {provide: ServiceC, useFactory: () => new ServiceC)} + {provide: ServiceD, useClass: ServiceE}, + ] + ... +}) +class Child { + construction(injector: Injector) {} + static ngComponentDef = defineComponent({ + ... + features: [ + ProvidesFeature( + [ + ServiceA, + {provide: ServiceB, useValue: 'someServiceBValue'}, + ],[ + {provide: ServiceC, useFactory: () => new ServiceC())} + {provide: ServiceD, useClass: ServiceE}, + ] + ) + ] + }); + ... +} +``` + +The above will create the following layout: + +| Index | `LViewData` | `TView.data` +| ----: | ------------ | ------------- +| `HEADER` +| `CONSTS` +| 10 | `[, ...]` | `{type: Element, index: 10, parent: null, expandoIndex: 11, directivesIndex: 19, providersIndex: 20, viewProvidersIndex: 22, expandoEnd: 23}` +| `VARS` +| `EXPANDO` +| 11..18| cumulativeBloom | templateBloom +| | *sub-section: `component` and `directives`* +| 19 | `factory(Child.ngComponentDef.factory)`* | `Child` +| | *sub-section: `providers`* +| 20 | `factory(ServiceA.ngInjectableDef.factory)`* | `ServiceA` +| 22 | `'someServiceBValue'`* | `ServiceB` +| | *sub-section: `viewProviders`* +| 22 | `factory(()=> new Service())`* | `ServiceC` +| 22 | `factory(()=> directiveInject(ServiceE))`* | `ServiceD` +| ... | ... | ... + +NOTICE: +- `*` denotes initial value copied from the `LProtoViewData`, as the tokens get instantiated the factories are replaced with actual value. +- That `TView.data` has `expando` and `expandoInjectorCount` properties which point to where the element injection data is stored. +- That all injectable tokens are stored in linear sequence making it easy to search for instances to match. +- That `directive` sub-section gets eagerly instantiated. + +Where `factory` is a function which wraps the factory into object which can be monomorphically detected at runtime in an efficient way. +```TypeScript +class Factory { + /// Marker set to true during factory invocation to see if we get into recursive loop. + /// Recursive loop causes an error to be displayed. + resolving = false; + constructor(public factory: Function) { } +} +function factory(fn) { + return new Factory(fn); +} +const FactoryPrototype = Factory.prototype; +function isFactory(obj: any): obj is Factory { + // See: https://jsperf.com/instanceof-vs-getprototypeof + return typeof obj === 'object' && Object.getPrototypeOf(obj) === FactoryPrototype; +} +``` + +Pseudo code: +1. Check if bloom filter has the value of the token. (If not exit) +2. Locate the token in the expando honoring `directives`, `providers` and `viewProvider` rules by limiting the search scope. +3. Read the value of `lViewData[index]` at that location. + - if `isFactory(lViewData[index])` then mark it as resolving and invoke it. Replace `lViewData[index]` with the value returned from factory (caching mechanism). + - if `!isFactory(lViewData[index])` then return the cached value as is. + +# `EXPANDO` and Injecting Special Objects. + +There are several special objects such as `ElementRef`, `ViewContainerRef`, etc... +These objects behave as if they are always included in the `providers` array of every component and directive. +Adding them always there would prevent tree shaking so they need to be lazily included. + +NOTE: +An interesting thing about these objects is that they are not memoized `injector.get(ElementRef) !== injector.get(ElementRef)`. +This could be considered a bug, it means that we don't have to allocate storage space for them. + +```typescript +@Injectable({ + provideIn: '__node__' as any // Special token not available to the developer + useFactory: injectElementRef // existing function which generates ElementRef +}) +class ElementRef { + ... +} +``` + +Consequence of the above is that `injector.get(ElementRef)` returns an instance of `ElementRef` without `Injector` having to know about `ElementRef` at compile time. + +# `EXPANDO` and Injecting the `Injector`. + +`Injector` can be injected using `inject(Injector)` method. +To achieve this in tree shakable way we can declare the `Injector` in this way: + +```typescript +@Injectable({ + provideIn: '__node_injector__' as any // Special token not available to the developer +}) +class Injector { + ... +} +``` + +NOTE: +We can't declare `useFactory` in this case because it would make the generic DI system depend on the Ivy renderer. +Instead we have unique token `'__node_injector__'` which we use for recognizing the `Injector` in tree shakable way. + +## `inject` Implementation + +A pseudo-implementation of `inject` function. + +```typescript +function inject(token: any): any { + let injectableDef; + if (typeof token === 'function' && injectableDef = token.ngInjectableDef) { + const provideIn = injectableDef.provideIn; + if (provideIn === '__node__') { + // if it is a special object just call its factory + return injectableDef.useFactory(); + } else if (provideIn === '__node_injector__') { + // if we are injecting `Injector` than create a wrapper object around the inject but which + // is bound to the current node. + return createInjector(); + } + } + return lookupTokenInExpando(token); +} +``` + + + +# `LContainer` + +TODO + +## Combining `LContainer` with `LViewData` + +TODO \ No newline at end of file