angular中利用zone避归没必要的开销提高程序的性能demo
在我们的最新文章中,我们讨论了如何通过探索Angular的ChangeDetectionStrategy
API以及如何分离变化探测器等技巧来使我们的Angular应用更快。虽然我们覆盖了许多不同的选项来提高演示应用程序的性能,但我们当然没有谈到所有可能的选择。
另一种选择是利用Zone API,在Angular Zone之外执行我们的代码,这将阻止Angular运行不必要的更改检测任务。他甚至投入时间和精力去创造一个展示如何做到这一点的演示。
在本文中,我们将探索他的普及,并解释如何使用Zone使我们的演示应用程序执行近60帧/秒
在我们直接进入代码之前,我们先来看看正在运行的应用程序的演示程序。作为一个快速回顾:这个想法是渲染10.000可拖动的SVG框。渲染10.000个盒子并不是一个非常复杂的任务,但是,挑战在于尽可能平滑拖拽的体验。换句话说,我们的目标是60 fps(每秒帧数),考虑到Angular在一个事件触发时(默认情况下)会重新渲染所有10.000个盒子,这确实很具挑战性。
- 这是一个优化前的实现:https://embed.plnkr.co/UBI5Sc5eDMpkDDkJfeGX
- 这是Jordi利用Angular的zone API 优化后的版本:https://embed.plnkr.co/GIf9sPuuZRLKwYK7mTfr
即使差异相当微妙,优化的版本在每帧的JavaScript执行方面表现也会更好。稍后我们会看看一些数字,但是让我们快速回顾Zones,然后深入代码并讨论Jordi如何使用Angular的NgZone
API来首先实现这种性能
Zone
在我们使用Zone API,特别是Angular的API之前NgZone
,我们需要了解Zone的实际情况,这里有两篇文章:
- 了解Zone
- Angular中的Zone
Zones打包异步浏览器API,并在异步任务已经开始或结束时通知消费者。Angular利用这些API在任何异步任务完成时得到通知。这包括诸如XHR
电话,setTimeout()
和几乎所有的用户事件,比如click
,submit
,mousedown
,...等等。
一旦通知,Angular知道它必须执行更改检测,因为任何异步操作可能已经改变了应用程序状态。例如,当我们使用Angular的Http
服务从远程服务器获取数据时,情况总是如此。以下片段显示了这样的调用如何改变应用程序状态:
@Component(...) export class AppComponent { data: any; // initial application state constructor(private dataService: DataService) {} ngOnInit() { this.dataService.fetchDataFromRemoteService().subscribe(data => { this.data = data // application state has changed, change detection needs to run now }); } }
关于这一点的好处是我们作为开发人员不必关心通知Angular执行更改检测,因为Zones会为我们做Angular订阅它们。
好的,现在我们谈谈这个问题,让我们来看看如何使用它们来快速构建我们的演示程序。
我们知道,只要发生异步事件并将事件处理程序绑定到该事件,就会执行更改检测。我们来看一下AppComponent
模板:
Component({ ... template: ` <svg (mousedown)="mouseDown($event)" (mouseup)="mouseUp($event)" (mousemove)="mouseMove($event)"> <svg:g box *ngFor="let box of boxes" [box]="box"> </svg:g> </svg> ` }) class AppComponent { ... }
3个事件处理程序绑定到外部的SVG元素。当这些事件中的任何一个触发并且它们的处理程序已经被执行时,则执行改变检测。实际上,这意味着Angular会运行变化检测,即使我们只是将鼠标移动到框上而不实际拖动一个box!
这是利用NgZone
APIs派上用场的地方了。NgZone
使我们能够明确地运行Angular的区域以外的某些代码,防止Angular运行任何变化检测。所以基本上,处理程序仍然会被执行,但是由于它们不会在Angular的区域内运行,Angular将不会得到任务完成的通知,因此不会执行变更检测。一旦我们释放我们正在拖动的方框,我们只想运行变化检测。
好的,我们如何做到这一点?我们所要做的就是确保mouseMove()
事件处理程序只在Angular的区域之外被附加和执行。除此之外,我们知道只有在选择一个框进行拖动时,我们才会附加该事件处理程序。换句话说,我们需要更改我们的mouseDown()
事件处理程序,以便将事件侦听器强制添加到文档中。
以下是这个样子:
import { Component, NgZone } from '@angular/core'; @Component(...) export class AppComponent { ... element: HTMLElement; constructor(private zone: NgZone) {} mouseDown(event) { ... this.element = event.target; this.zone.runOutsideAngular(() => { window.document.addEventListener('mousemove', this.mouseMove.bind(this)); }); } mouseMove(event) { event.preventDefault(); this.element.setAttribute('x', event.clientX + this.clientX + 'px'); this.element.setAttribute('y', event.clientX + this.clientY + 'px'); } }
我们在事件处理程序中注入NgZone
和调用,runOutsideAngular()
在这个mouseDown()
事件处理程序中附加事件的事件处理程序mousemove
。这确保mousemove
事件处理程序确实只在选定框时附加到文档。此外,我们保存到点击框的基本DOM元素的引用,所以我们可以更新它的x
和y
在属性mouseMove()
方法。我们正在处理DOM元素,而不是绑定了x
和的box对象y
,因为我们在Angular的区域之外运行代码,绑定不会被检测到。换句话说,我们不更新DOM,所以我们可以看到box是移动的,但是我们实际上还没有更新box。
另外请注意,我们mouseMove()
从组件的模板中删除了绑定。我们也可以删除mouseUp()
处理程序,并强制附加它,就像我们mouseMove()
处理程序一样。但是,它不会增加性能方面的任何价值,所以我们决定为了简单起见将其保留在模板中:
<svg (mousedown)="mouseDown($event)" (mouseup)="mouseUp($event)"> <svg:g box *ngFor="let box of boxes" [box]="box"> </svg:g> </svg>
在接下来的步骤中,我们要确保每当我们释放一个box(mouseUp
)时,我们更新box模型,再加上,我们想要执行变化检测,以便模型再次与视图同步。很酷的事情NgZone
不仅仅是它允许我们在Angular的Zone以外运行代码,它还提供了在Angular Zone 内部运行代码的API ,最终会导致Angular再次执行变化检测。我们所要做的就是打回调NgZone.run()
给它应该执行的代码。
这里是我们更新的mouseUp()
事件处理程序:
@Component(...) export class AppComponent { ... mouseUp(event) { // Run this code inside Angular's Zone and perform change detection this.zone.run(() => { this.updateBox(this.currentId, event.clientX + this.offsetX, event.clientY + this.offsetY); this.currentId = null; }); window.document.removeEventListener('mousemove', this.mouseMove); } }
具体表现
优化前
优化后
结论
使用区域是摆脱Angular变化检测的好方法,不需要分离变化检测器,也不会使应用程序代码过于复杂。事实上,事实证明,Zones API非常易于使用,特别是NgZone
在Angular外部或内部运行代码的API。根据这些数字,我们甚至可以说这个版本与我们之前文章中提出的最快解决方案一样快。考虑到开发人员在使用Zones API时的体验要好得多,因为它比使用手动分离和重新连接更改探测器引用更容易使用,这绝对是迄今为止最“美观”的性能改进。