parent
d5bd86ae5d
commit
86a3be8610
|
@ -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: `<div>Hello <b>World</b>!</div>`
|
||||||
|
})
|
||||||
|
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 | `<div>` | `{type: Element, index: 10, parent: null}`
|
||||||
|
| 11 | `#text(Hello )` | `{type: Element, index: 11, parent: tView.data[10]}`
|
||||||
|
| 12 | `<b>` | `{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: `<div title="{{name}}">Hello {{name}}!</div>`
|
||||||
|
})
|
||||||
|
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 | `<div>` | `{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: `<child tooltip></child>`
|
||||||
|
})
|
||||||
|
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 | `[<child>, ...]` | `{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 (`<child>`)
|
||||||
|
| 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` | `\* <child> *\ 10`
|
||||||
|
| `2` | `21` | `\* new Child() *\ 19` | `\* <child> *\ 10`
|
||||||
|
| `Child.ngComponentDef.hostBindings` | invoke with => | `\* new Child() *\ 19` | `\* <child> *\ 10`
|
||||||
|
| | `21` | `\* new Tooltip() *\ 20` | `\* <child> *\ 10`
|
||||||
|
| `Child.ngComponentDef.hostVars` | `22` | `\* new Tooltip() *\ 20` | `\* <child> *\ 10`
|
||||||
|
| `Tooltip.ngDirectiveDef.hostBindings` | invoke with => | `\* new Tooltip() *\ 20` | `\* <child> *\ 10`
|
||||||
|
| | `22` | `21` | `\* <child> *\ 10`
|
||||||
|
| `Tooltip.ngDirectiveDef.hostVars` | `22` | `21` | `\* <child> *\ 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: `<child></child>`
|
||||||
|
})
|
||||||
|
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 | `[<child>, ...]` | `{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
|
Loading…
Reference in New Issue