最近总结了一些能够让 Component 更加优雅的方法,不妨在这里分享一下。当然,你可以引入 NgRx, 然后把逻辑都写到 Reducer 里去,这也是挺优雅的。但是这里想存粹的探讨一下 Component 的一些优化方法。
OnPush Detection
changeDetection: ChangeDetectionStrategy.OnPush,怎么说呢,我们可以尽可能是在新的 component 设置成,onPush, 减少对于脏检查的依赖。脏检查对于我们来说,很多时候像是 Magic thing,我们也弄不清楚怎么回事,尽可能减少对于脏检查的依赖一方面可以提升性能,另一方面也能够帮助我们更加清晰的了解,HTML 什么时候会重新渲染,什么时候不会。当然,使用 OnPush 的是有前提的,需要我们想办法告知 Angular 变化已经发生。后面会慢慢讲,不管怎么样,可以在新的 Feature 中设置起来,然后会发现一些以前能成功渲染的例子现在不行了,可以尝试找一找原因,至少也能够帮助你对于脏检查机制有更加深入的理解。
Event Trigger => Observable
我们在开发过程中,使用 Event 绑定函数是非常常见的套路,(click)="onClickHandler($event)",类似这样的写法。一种有效的思维方式是将 Event 转换为 Event 流来处理:
fromEvent(this.elementRef.nativeElement, 'click').pipe(
switchMap(() => {
//xxxxxxxxxxxxxx
})
)
当然,这样的好处就是,有更灵活的处理方式,switchMap就是其中一个例子。慢慢会细讲。
LifeCycle 函数 => Observable
这个优势其实也是很明显的,React,Vue 其实也都采用了类似的策略,就是,我们不再需要将逻辑拆分到不同的 Life Cycle 函数中,减少重复逻辑的调用,更加直觉化的变成体验。举个例子:
private ngViewInit$ = new Subject<boolean>();
private ngDestroy$ = new Subject<boolean>();
private ngInit$ = new Subject<boolean>();
//.....
//.....
ngAfterViewInit(): void {
this.ngViewInit$.next(true);
}
ngOnDestroy(): void {
this.ngDestroy$.next(true);
}
这样,我们原本要分拆到不同 Life Cycle 函数中的代码就可以写到一起,比如 Event 的监听,有一个非常简单的需求就是,页面加载的时候显示 List, 点击 Refresh Button 的时候,同时再显示 List:
const refreshButtonEvent$ = this.ngViewInit$.pipe(
switchMapTo(fromEvent(this.elementRef.nativeElement, 'click')),
)
const list$ = merge(this.ngInit$, refreshButtonEvent$);
其实不难发现,你不再需要把需求翻译成编程的 click 事件,而是像自然语言一样的顺序编程。
当然,目前的缺点是,Life Cycle 函数的转换目前还是需要手动来实现。
Component 中区分 View Model 变量和其他变量:
怎么解释呢?因为 Angular 是绑定 Component class 的 this 到 Template 中的,这就带来一个问题。有些 property 我们定义了是为了在 Template 中显示,有些变量,我们存粹是为了能够在不同的 method 能够调用,共享状态。
其实,其中一个改进方法是,只有 Template 中要用的才定义成 public, 其他都是 private。其实这样依然还是造成每一个 Component 有特别多的 properties。本质上,还是因为太方便了,导致定义的人往往不假思索就定义一个 property 作为 Component 全局使用,其实,这很类似全局变量的使用,不方便逻辑的拆分。
最近尝试学习 React 中的方式,所有要在 Template 中使用的 property 都会放在 State 中,一方面是减少 public property 的定义,另一方面,也可以一目了然,哪些是用在 Template 中的。其实还有一个好处就是可以把 Observable combine 成一个,这样就避免了在 Template 中有太多 async pipe。举个例子:
const foods$ = xxxxx
const users$ = xxxxx
this.state$ = combineLatest(foods$, users$);
当然,这样定义 State 有一些问题:
- State 是一个数组,而不是 Object
- State 的 first emit 是等所有的 observable 至少 emit 一次,这其实不是我们预期的。 我们的默认初始值可能更加类似于:
{
foods: null,
users: null,
}
// .....
{
foods: [....],
users: null,
}
//.....
{
foods: [....],
users: [....],
}
当然,这其实也很容易解决:
function createState(observables: Observable<any>[], projector: (subStates: Array<any>) => { [key: string]: any }) {
const initialState$ = combineLatest(
observables.map((observable) => observable.pipe(startWith(null)))
);
return projector ? initialState$.pipe(map(projector)) : initialState$;
}
使用起来也很方便:
this.state$ = createState([foods$, users$], ([foods, users]) => ({
foods,
users,
}));
<div *ngIf="state$ | async as state">
{{ state.foods | json }}
{{ state.users | json }}
</div>
如果能够保证 Template 中所有的数据都能够从 State 或者 @Input 中获取,State 又能够保证,所有的数据源都是 observable,不直接从 this 上取得普通变量,我们就可以很放心的,在 Component 上加上 ChangeDetectionStrategy.OnPush。因为这时候,只有 component props 发生变化或者 State emit new state 的时候,template 才有可能需要重新渲染。
尽可能使用 immutable
其实在使用 Angular 的时候,我们经常会有 immutable 的困扰,举个简单的例子:
// parent.component.ts
this.users.push(new User());
// parent.component.html
<app-child [users]="users"></app-child>
----------------------------------------
// child.component.ts
ngOnChanges(changes: SimpleChanges) {
console.log(changes?.users?.currentValue)
}
// child.component.html
{{users | json}}
你会发现一个奇怪的现象,就是,child component 能够正确的现实新加的 user,但是 ngOnChanges 却没有 detect 到改动。这是什么原因呢?
其实很简单,因为 Angular 的脏检查已经足够只能,脏检查重新渲染了 users, 但是,因为我们使用了 push, change detection 却没有感知到数据的变化。如果你设置成 OnPush 就会发现,页面也不会变化了。Template 跟实际代码的不一致,就会出现一些我们意想不到的问题。所以我们会推荐你不要使用数组的 push,而是使用:
this.users = [...users, new User];
在前端,因为动态渲染的存在,其实我们都不太推荐直接修改 array 或者 object 本身,永远应该在改动后返回新的地址。... 固然简化了我们的操作,如果使用 immutible.js 或者 immer.js 就会方便很多。
尽可能少些,或者不写 subscribe
目前 Angular 本身并没有提供 lifeCycle 的 observeble, 所以,理论上,我们还是需要每次手动在 component 的 destroy 函数中去 unsubscribe 一个 subscription。有时候我们忘记去 unsubscribe, 有时候我们为了某个 subscription 是否需要 unsubscribe 争论的喋喋不休。我们来看一个例子:
ngOnInit(): void {
this.userService.get().subscribe((users) => {
this.users = users;
});
}
首先,one emit value 是否需要 unsubscribe,本身就是个争论不休的话题。假设,我们再加上 unsubscribe 的逻辑:
ngOnInit(): void {
this.subscriptions.push(
this.userService.get().subscribe((users) => {
this.users = users;
})
);
}
ngOnDestroy(): void {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
你会发现,逻辑大大的变得复杂了。使用 async pipe 让平台来处理就可以大大的简化逻辑。
其实,在工作中发现,很多时候发现,使用过程中甚至有不少 subscribe 的嵌套,其实这跟 promise 中 then 前套是一个意思,应该尽可能的避免。
如果你发现,在代码中写了不少类似的代码:
this.service.getUser('zhangsan')
.subscribe(user => {
user.gender = 'male';
this.service.updateUser(user).subscribe(user => {
this.isUserUpdated = true;
});
}, () => {
this.isUserUpdated = false;
});
这种写法就变得非常不 observable。违背了,Angular 将 promise 转化为 observable 的初衷,如果熟悉这种写法,倒不如,转化成 promise 更加符合思路,也不用纠结 unsubscribe 的问题:
try {
const user = await this.service.getUser('zhangsan').toPromise();
this.isUserUpdated = await this.service.updateUser({...user, gender: 'male'});
} catch(){
this.isUserUpdated = false;
}
当然,我们也可以把代码写的更加 observable,也会体现出它的优势:
this.isUserUpdated$ = this.service.getUser('zhangsan').pipe(
switchMap(user => this.service.updateUser({ ...user, gender: 'male' })),
map(() => true;);
catchError(() => of(false));
)
不管怎么样,异步处理减少嵌套都会让代码变得更加简单清晰。曾经有人说过,如果你发现你的代码中出现 subscribe 嵌套,甚至在同一个 component 中,写了超过两次的 subscribe, 可能你就需要重新阅读一下你的代码了。