refactor(ivy): move hostVars/hostAttrs from instruction to DirectiveDef (#34683)

This change moves information from instructions to declarative position:
- `ɵɵallocHostVars(vars)` => `DirectiveDef.hostVars`
- `ɵɵelementHostAttrs(attrs)` => `DirectiveDef.hostAttrs`

When merging directives it is necessary to know about `hostVars` and `hostAttrs`. Before this change the information was stored in the `hostBindings` function. This was problematic, because in order to get to the information the `hostBindings` would have to be executed. In order for `hostBindings` to be executed the directives would have to be instantiated. This means that the directive instantiation would happen before we had knowledge about the `hostAttrs` and as a result the directive could observe in the constructor that not all of the `hostAttrs` have been applied. This further complicates the runtime as we have to apply `hostAttrs` in parts over many invocations.

`ɵɵallocHostVars` was unnecessarily complicated because it would have to update the `LView` (and Blueprint) while existing directives are already executing. By moving it out of `hostBindings` function we can access it statically and we can create correct `LView` (and Blueprint) in a single pass.

This change only changes how the instructions are generated, but does not change the runtime much. (We cheat by emulating the old behavior by calling `ɵɵallocHostVars` and `ɵɵelementHostAttrs`) Subsequent change will refactor the runtime to take advantage of the static information.

PR Close #34683
This commit is contained in:
Miško Hevery
2020-01-08 11:32:33 -08:00
parent 94504ff5c8
commit 2961bf06c6
33 changed files with 751 additions and 185 deletions

View File

@ -7,6 +7,8 @@
*/
import {Component, ContentChildren, Directive, EventEmitter, HostBinding, HostListener, Input, OnChanges, Output, QueryList, ViewChildren} from '@angular/core';
import {ivyEnabled} from '@angular/core/src/ivy_switch';
import {getDirectiveDef} from '@angular/core/src/render3/definition';
import {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {onlyInIvy} from '@angular/private/testing';
@ -42,6 +44,86 @@ describe('inheritance', () => {
}).toThrowError('Directives cannot inherit Components');
});
describe('multiple children', () => {
it('should ensure that multiple child classes don\'t cause multiple parent execution', () => {
// Assume this inheritance:
// Base
// |
// Super
// / \
// Sub1 Sub2
//
// In the above case:
// 1. Sub1 as will walk the inheritance Sub1, Super, Base
// 2. Sub2 as will walk the inheritance Sub2, Super, Base
//
// Notice that Super, Base will get walked twice. Because inheritance works by wrapping parent
// hostBindings function in a delegate which calls the hostBindings of the directive as well
// as super, we need to ensure that we don't double wrap the hostBindings function. Doing so
// would result in calling the hostBindings multiple times (unnecessarily). This would be
// especially an issue if we have a lot of sub-classes (as is common in component libraries)
const log: string[] = [];
@Directive({selector: '[superDir]'})
class BaseDirective {
@HostBinding('style.background-color')
get backgroundColor() {
log.push('Base.backgroundColor');
return 'white';
}
}
@Directive({selector: '[superDir]'})
class SuperDirective extends BaseDirective {
@HostBinding('style.color')
get color() {
log.push('Super.color');
return 'blue';
}
}
@Directive({selector: '[subDir1]'})
class Sub1Directive extends SuperDirective {
@HostBinding('style.height')
get height() {
log.push('Sub1.height');
return '200px';
}
}
@Directive({selector: '[subDir2]'})
class Sub2Directive extends SuperDirective {
@HostBinding('style.width')
get width() {
log.push('Sub2.width');
return '100px';
}
}
@Component({template: `<div subDir1 subDir2></div>`})
class App {
}
TestBed.configureTestingModule({
declarations: [App, Sub1Directive, Sub2Directive, SuperDirective],
});
const fixture = TestBed.createComponent(App);
fixture.detectChanges(false); // Don't check for no changes (so that assertion does not need
// to worry about it.)
expect(log).toEqual([
'Base.backgroundColor', 'Super.color', 'Sub1.height', //
'Base.backgroundColor', 'Super.color', 'Sub2.width', //
]);
if (ivyEnabled) {
expect(getDirectiveDef(BaseDirective) !.hostVars).toEqual(1);
expect(getDirectiveDef(SuperDirective) !.hostVars).toEqual(2);
expect(getDirectiveDef(Sub1Directive) !.hostVars).toEqual(3);
expect(getDirectiveDef(Sub2Directive) !.hostVars).toEqual(3);
}
});
});
describe('ngOnChanges', () => {
it('should be inherited when super is a directive', () => {
const log: string[] = [];