rx-angular introduction
Who am I
- Name: Jia Li
- Company: ThisDot
- Zone.js: Code Owner
- Angular: Collaborator
rx-angular
RxAngular is a reactive library providing extensions for developing high performant applications.
Created by Michael Hladky
Angular Change Detection
Trigger
1. check
2. render
Trigger Change Detection
- Auto: Zone.js
- Manual
Zone.js
<button (click)="clickHandler()"></button>
ngZone.run(() => {
btn.addEventListener_zonePatchedVersion('click', clickHandler);
});
btn.addEventListener('click' => {
clickHandler(...);
onMicrotaskEmpty.emit(null);
});
// ApplicationRef
ngZone.onMicrotaskEmpty.subscribe(() => {
// run Change Detection from root
zone.run(() => tick());
});
Zone.js
- setTimeout/setInterval
- promise.then
- XMLHttpRequest
- addEventListener
- ....
Pull vs Push based CD
const changePossibleTimings = [XHR, setTimeout, promise.then,
addEventListener, ...];
changePossibleTimings.forEach(t => t.whenCallback(detectChanges();));
Pull base
Push base
change$.subscribe(() => detectChanges());
Other Zone.js issues
- Unwanted Change detections
- UnPatched new async APIs
- native async/await issue (ES2017+)
Async Pipe?
@Component({
...,
template: `<div>{{data$ | async}}</div>`
})
export class AppComponent {
data$: Observable<number> = of(1);
}
OnPush?
@Component({
...,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
@Input() input;
}
Angular View Tree
Root
Comp A
Comp B
Comp Sub1
Comp Sub2
Comp Sub3
Comp Sub4
ApplicationRef.tick()
Root
Comp A
Comp B
Comp Sub1
Comp Sub2
Comp Sub3
Comp Sub4
ChangeDetectionStrategy.Default
Root
Comp A
Default
Comp B
Comp Sub1
Comp Sub2
Comp Sub3
Comp Sub4
CHECK ALWAYS
ChangeDetectionStrategy.onPush
Root
Comp A
onPush
Comp B
Comp Sub1
Comp Sub2
Comp Sub3
Comp Sub4
ChangeDetectionStrategy.onPush
AppComponent
OnPush
Component
OnPush
@Component({
template: `<button (click)="click()"></button>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushComponent {
@Input() input;
@Output() output = new EventEmitter();
click() {}
}
<app-push [input]="inputData" (output)="FromPush($event)"></app-push>
When changes happen from the items declared in the template
<app-push></app-push>
@Component({
template: `<button #btn></button>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushComponent {
@Input() input;
@Output() output = new EventEmitter();
@ViewChild('btn') btn;
ngOnInit() {
setTimeout(() => {
this.input = 'new value';
this.output.emit('new output');
});
this.btn.nativeElement.addEventListener('click', click);
}
ChangeDetectorRef.markForCheck()
Root
Comp A
onPush
Comp B
Comp Sub1
Comp Sub2
Comp Sub3
Comp Sub4
markForCheck()
ChangeDetectionRef.detectChanges()
Root
Comp A
Comp B
Comp Sub1
Comp Sub2
Comp Sub3
Comp Sub4
detectChanges()
ɵmarkDirty()
Root
Comp A
onPush
Comp B
Comp Sub1
Comp Sub2
Comp Sub3
Comp Sub4
ɵmarkDirty()
animationFrame
detach()/reattach()
Root
Comp A
Comp B
Comp Sub1
Comp Sub2
Comp Sub3
Comp Sub4
detach()
Async Pipe
@Pipe({name: 'async', pure: false})
export class AsyncPipe implements OnDestroy, PipeTransform {
obs$ = this.observablesToSubscribeSubject
.pipe(
distinctUntilChanged(ɵlooseIdentical),
switchAll(),
distinctUntilChanged(),
tap(v => { this.value = v; this.ref.markForCheck(); })
);
ngrx/component push pipe
@Pipe({name: 'push', pure: false})
export class PushPipe implements OnDestroy, PipeTransform {
obs$ = this.observablesToSubscribeSubject
.pipe(
distinctUntilChanged(ɵlooseIdentical),
switchAll(),
distinctUntilChanged(),
tap(v => {
this.value = v;
if (hasZone()) {
this.ref.markForCheck();
} else {
this.ref.detectChanges();
}
})
);
rx-angular push pipe
@Pipe({name: 'push', pure: false})
export class PushPipe implements OnDestroy, PipeTransform {
obs$ = this.observablesToSubscribeSubject
.pipe(
distinctUntilChanged(ɵlooseIdentical),
switchAll(),
distinctUntilChanged(),
tap(v => {
this.value = v;
this.ref.detectChanges();
})
);
ChangeDetection Strategies
- local
- global
- native
- noop
- detach (experimental)
<div *rxLet="list$; let list; strategy: 'local'"></div>
<hero-list heroes="list$ | push: 'global'"></hero-list>
local strategy
@Pipe({name: 'async', pure: false})
export class PushPipe implements OnDestroy, PipeTransform {
obs$ = this.observablesToSubscribeSubject
.pipe(
distinctUntilChanged(ɵlooseIdentical),
switchAll(),
distinctUntilChanged(),
tap(v => {
this.value = v;
this.ref.detectChanges();
})
);
global strategy
@Pipe({name: 'async', pure: false})
export class PushPipe implements OnDestroy, PipeTransform {
obs$ = this.observablesToSubscribeSubject
.pipe(
distinctUntilChanged(ɵlooseIdentical),
switchAll(),
distinctUntilChanged(),
tap(v => {
this.value = v;
markDirty(comp);
})
);
native strategy
@Pipe({name: 'async', pure: false})
export class PushPipe implements OnDestroy, PipeTransform {
obs$ = this.observablesToSubscribeSubject
.pipe(
distinctUntilChanged(ɵlooseIdentical),
switchAll(),
distinctUntilChanged(),
tap(v => {
this.value = v;
markForCheck();
})
);
noop strategy
@Pipe({name: 'async', pure: false})
export class PushPipe implements OnDestroy, PipeTransform {
obs$ = this.observablesToSubscribeSubject
.pipe(
distinctUntilChanged(ɵlooseIdentical),
switchAll(),
distinctUntilChanged(),
tap(v => {
this.value = v;
// do nothing
})
);
detach strategy
@Pipe({name: 'async', pure: false})
export class PushPipe implements OnDestroy, PipeTransform {
obs$ = this.observablesToSubscribeSubject
.pipe(
distinctUntilChanged(ɵlooseIdentical),
switchAll(),
distinctUntilChanged(),
tap(v => {
this.value = v;
this.ref.reattach();
this.ref.detectChanges();
this.ref.detach();
})
);
Coalescing
@Component({
template: `
<div (click)="parent()">
<button (click)="child()">Submit</button>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent {
parent() {
console.log('parent');
}
child() {
console.log('child');
}
}
platformBrowserDynamic()
.bootstrapModule(AppModule, { ngZoneEventCoalescing: true })
CoalesceWith Operator
AnimationFrame/PromiseTick/setTimeout/...
data$.pipe(
coalesceWith(animationDurationSelector),
switchMap((v) => renderTick.pipe(map(() => v))),
tap(renderMethod)
);
Multiple push pipes with scope
@Component({
selector: 'app-display',
template: `
{{id$ | push}}
{{firstName$ | push}}
{{lastName$ | push}}
`
})
export class DisplayComponent {
id$ = of(42);
firstName$ = of('John');
lastName$ = of('Doe');
}
const scope = component;
data$.pipe(
coalesceWith(animationDurationSelector, scope),
switchMap((v) => tick.pipe(map(() => v))),
tap(renderMethod)
);
viewport-prio directive
<div class="target" viewport-prio></div>
unpatch zone
<button [unpatch] (click)="click()">click me</button>
<div (scroll#passive,capture,ngManual)="onScroll()"></div>
Reactive context
<ng-container *rxLet="hero$; let hero;
rxSuspense: suspenseView; rxError: errorView; rxComplete: completeView">
{{hero.name}}
<ng-container>
<ng-template #suspenseView>Loading...</ng-template>
<ng-template #errorView>Error!</ng-template>
<ng-template #completeView>Complete.</ng-template>
New features
- Template View Cache
- Template rendered callback
- *rxFor
Performance benchmark
References
- https://github.com/rx-angular/rx-angular
- https://indepth.dev/angulars-push-pipe-part-1
- https://indepth.dev/angulars-push-pipe-part-2
rx-angular introduction
By jiali
rx-angular introduction
- 1,869