尝试使用 Angular NgZone 优化应用性能
使用 Angular 开发的过程中,可能会有意想不到的一些性能问题存在。
在一些异步调用比较频繁的代码中,如果不注意就可能就会导致页面卡顿。那么为了解决这个问题,我们需要通过 NgZone 来对某些一些比较频繁的异步操作进行隔离,并对其回调进行特定的响应。因为异步操作都会触发变更检测,而变更检测在此时对于我们来说有点多余,尽管文档上说,变更检测的速度很快,但是无用的变更检测对应用的性能是有影响的,所以为了减少这些无用的变更检测,就需要 NgZone。
NgZone API
为了使用 NgZone,我们需要了解一些其包含了那些接口,在这里,我们只需要关注以下四个接口。
typescript
run 函数
NgZone.run 接受三个参数,fn、applyThis、applyArgs。其中 fn 是具体调用的函数,applyThis 和 applyArgs是 fn 所绑定的上下文和具体传入的参数,其返回值是把 fn 的返回值作为返回值。
这个函数的作用是为了使 fn 中的异步任务能够重新回到 Angular Zone 中。那么就是说所有在调用的即将到来的宏任务和微任务都将在 Angular Zone 的上下文中执行(也就是触发变更检测),并且使这个函数可以同步运行。
runTask 函数
NgZone.runTask 接受三个参数,fn、applyThis、applyArgs。其中 fn 是具体调用的函数,applyThis 和 applyArgs是 fn 所绑定的上下文和具体传入的参数,其返回值是把 fn 返回的值作为返回值。
这个函数的作用是仍然是使 fn 中的异步任务能够重新回到 Angular Zone 中。但是有一点,fn这个函数是作为同步任务执行,也就是在执行完毕后,会触发一次变更检测。
runGuarded 函数
NgZone.runGuarded 接受三个参数,fn、applyThis、applyArgs。其中 fn 是具体调用的函数,applyThis 和 applyArgs是 fn 所绑定的上下文和具体传入的参数,其返回值是把 fn 返回的值作为返回值。
这个函数跟 run 一样,只不过它将异常捕获,并由 NgZone.onError 发出错误。
runOutsideAngular 函数
NgZone.runGuarded 接受三个参数,fn、applyThis、applyArgs。其中 fn 是具体调用的函数,applyThis 和 applyArgs是 fn 所绑定的上下文和具体传入的参数,其返回值是把 fn 返回的值作为返回值。
这个函数作用是将 fn 脱离 Angular Zone 来运行,这样在 fn 中调用异步函数时,就不会有变更检测。
例子一:拖拽事件
这个例子很好的说明了,并不是所有的异步事件都需要触发变更检测。
首先先看看代码:
typescript
我设计了很简单的一个拖拽的方案,拖拽结束后,会展示这个 div 的 top。虽然这个方案没有正确计算拖拽结束后的位置,但是这并不影响我想要说明的结果。很显然,我只需要在 dragend 中触发一次变更检测,就能完成我们想要的结果,但是可以看看控制台,一次简单的拖拽会触发上百次的变更检测。
那么为了减少拖拽过程中的变更检测,我们通过依赖注入,获取到 NgZone 的实例,并且,将 dragstart & drag事件放入 runOutsideAngular 中:
constructor(private zone: NgZone) {}
ngOnInit() {
...
this.zone.runOutsideAngular(() => {
div.addEventListener('dragstart', (event) => {
const target = (event.target as HTMLDivElement);
target.style.opacity = '1';
target.style.cursor = 'pointer';
dragged = target;
});
div.addEventListener('drag', (event: DragEvent) => {
event.preventDefault();
const target = (event.target as HTMLDivElement);
target.style.opacity = '0';
target.style.cursor = 'pointer';
});
});
...
}
这样再看,每次拖拽都只会打印一次 hello,那也就说明了,只触发了一次变更检测。
例子二:拖拽封装
为了更好的提供复用性,有时候我们对外暴露的只是一个简单的Observable,那么使用起来可能就是对Observable 进行 subscribe 。
class Dragger {
private drag = new BehaviorSubject<DragEvent | null>(null);
private dragStart = new BehaviorSubject<DragEvent | null>(null);
private dragEnd = new BehaviorSubject<DragEvent | null>(null);
drag$: Observerable<DragEvent> = drag.asObservable();
dragStart$: Observerable<DragEvent> = dragStart.asObservable();
dragEnd$: Observerble<DragEvent> = dragEnd.asObservable();
constructor(private div: HTMLDivElement) {
div.addEventListener('dragstart', (event) => {
const target = (event.target as HTMLDivElement);
target.style.opacity = '1';
target.style.cursor = 'pointer';
dragStart.next(event);
});
div.addEventListener('drag', (event: DragEvent) => {
event.preventDefault();
const target = (event.target as HTMLDivElement);
target.style.opacity = '0';
target.style.cursor = 'pointer';
drag.next(event);
});
div.addEventListener('dragend', (event: DragEvent) => {
const target = (event.target as HTMLDivElement);
target.style.opacity = '1';
target.style.top = `${event.y}px`;
target.style.left = `${event.x}px`;
dragEnd.next(event);
});
}
}
这样进行封装后,我们在组件中就可以这样调用:
typescript
但是,例子一的问题仍然存在,所以我们需要这样初始化我们的 dragger 。
typescript
但是这样做会有问题,dragEnd$ 并不能触发变更检测,也就是 top 不会在拖拽后更新。为了能够触发同步的变更检测,我们需要这样做。
typescript
总结
本文通过具体的例子总结了 NgZone 的具体使用方法。其主要目的还是为了优化变更检测机制,以便能应用的优化性能。