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:

committed by
Andrew Kushnir

parent
469aba0140
commit
737506e79c
@ -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[] = [];
|
||||
|
||||
|
Reference in New Issue
Block a user