fix(core): Allow modification of lifecycle hooks any time before bootstrap (#35464)

Currently we read lifecycle hooks eagerly during `ɵɵdefineComponent`.
The result is that it is not possible to do any sort of meta-programing
such as mixins or adding lifecycle hooks using custom decorators since
any such code executes after `ɵɵdefineComponent` has extracted the
lifecycle hooks from the prototype. Additionally the behavior is
inconsistent between AOT and JIT mode. In JIT mode overriding lifecycle
hooks is possible because the whole `ɵɵdefineComponent` is placed in
getter which is executed lazily. This is because JIT mode must compile a
template which can be specified as `templateURL` and those we are
waiting for its resolution.

- `+` `ɵɵdefineComponent` becomes smaller as it no longer needs to copy
  lifecycle hooks from prototype to `ComponentDef`
- `-` `ɵɵNgOnChangesFeature` feature is now always included with the
  codebase as it is no longer tree shakable.

Previously we have read lifecycle hooks from prototype in the
`ɵɵdefineComponent` so that lifecycle hook access would be monomorphic.
This decision was made before we had `T*` data structures. By not
reading the lifecycle hooks we are moving the megamorhic read form
`ɵɵdefineComponent` to instructions. However, the reads happen on
`firstTemplatePass` only and are subsequently cached in the `T*` data
structures. The result is that the overall performance should be same
(or slightly better as the intermediate `ComponentDef` has been
removed.)

- [ ] Remove `ɵɵNgOnChangesFeature` from compiler. (It will no longer
      be a feature.)
- [ ] Discuss the future of `Features` as they hinder meta-programing.

Fix #30497

PR Close #35464
This commit is contained in:
Misko Hevery
2020-02-14 18:02:11 -08:00
committed by Andrew Kushnir
parent 469aba0140
commit 737506e79c
13 changed files with 264 additions and 147 deletions

View File

@ -1124,6 +1124,100 @@ describe('onChanges', () => {
});
});
describe('meta-programing', () => {
it('should allow adding lifecycle hook methods any time before first instance creation', () => {
const events: any[] = [];
@Component({template: `<child name="value"></child>`})
class App {
}
@Component({selector: 'child', template: `empty`})
class Child {
@Input() name: string = '';
}
const ChildPrototype = Child.prototype as any;
ChildPrototype.ngOnInit = () => events.push('onInit');
ChildPrototype.ngOnChanges = (e: SimpleChanges) => {
const name = e['name'];
expect(name.previousValue).toEqual(undefined);
expect(name.currentValue).toEqual('value');
expect(name.firstChange).toEqual(true);
events.push('ngOnChanges');
};
ChildPrototype.ngDoCheck = () => events.push('ngDoCheck');
ChildPrototype.ngAfterContentInit = () => events.push('ngAfterContentInit');
ChildPrototype.ngAfterContentChecked = () => events.push('ngAfterContentChecked');
ChildPrototype.ngAfterViewInit = () => events.push('ngAfterViewInit');
ChildPrototype.ngAfterViewChecked = () => events.push('ngAfterViewChecked');
ChildPrototype.ngOnDestroy = () => events.push('ngOnDestroy');
TestBed.configureTestingModule({
declarations: [App, Child],
});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.destroy();
expect(events).toEqual([
'ngOnChanges', 'onInit', 'ngDoCheck', 'ngAfterContentInit', 'ngAfterContentChecked',
'ngAfterViewInit', 'ngAfterViewChecked', 'ngOnDestroy'
]);
});
it('should allow adding lifecycle hook methods with inheritance any time before first instance creation',
() => {
const events: any[] = [];
@Component({template: `<child name="value"></child>`})
class App {
}
class BaseChild {}
@Component({selector: 'child', template: `empty`})
class Child extends BaseChild {
@Input() name: string = '';
}
// These are defined on the base class
const BasePrototype = BaseChild.prototype as any;
BasePrototype.ngOnInit = () => events.push('onInit');
BasePrototype.ngOnChanges = (e: SimpleChanges) => {
const name = e['name'];
expect(name.previousValue).toEqual(undefined);
expect(name.currentValue).toEqual('value');
expect(name.firstChange).toEqual(true);
events.push('ngOnChanges');
};
// These will be overwritten later
BasePrototype.ngDoCheck = () => events.push('Expected to be overbidden');
BasePrototype.ngAfterContentInit = () => events.push('Expected to be overbidden');
// These are define on the concrete class
const ChildPrototype = Child.prototype as any;
ChildPrototype.ngDoCheck = () => events.push('ngDoCheck');
ChildPrototype.ngAfterContentInit = () => events.push('ngAfterContentInit');
ChildPrototype.ngAfterContentChecked = () => events.push('ngAfterContentChecked');
ChildPrototype.ngAfterViewInit = () => events.push('ngAfterViewInit');
ChildPrototype.ngAfterViewChecked = () => events.push('ngAfterViewChecked');
ChildPrototype.ngOnDestroy = () => events.push('ngOnDestroy');
TestBed.configureTestingModule({
declarations: [App, Child],
});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.destroy();
expect(events).toEqual([
'ngOnChanges', 'onInit', 'ngDoCheck', 'ngAfterContentInit', 'ngAfterContentChecked',
'ngAfterViewInit', 'ngAfterViewChecked', 'ngOnDestroy'
]);
});
});
it('should call all hooks in correct order when several directives on same node', () => {
let log: string[] = [];