为什么绝不应在 Angular Template中使用函数?
这篇文章已经很好的解释了这个问题,我们就不重复造轮子了,下面直接翻译一下这篇文章。https://medium.com/showpad-engineering/why-you-should-never-use-function-calls-in-angular-template-expressions-e1a50f9c0496
Angular Template 功能是非常强大的。 我们可以通过 Directive 和 属性绑定,创造出非常复杂且结构清晰的视图:
<ng-container *ngIf="isLoggedIn"> <h1>Welcome {{ fullName }}!</h1> </ng-container>
正因为Angular Template 如此强大,所以当我们的视图变得更加复杂时,Angular Template 也很容易的变复杂。最终在Angular Template 中使用了函数:
<ng-container *ngIf="isLoggedIn"> <h1>Welcome {{ fullName }}!</h1> <a href="files" *ngIf="hasAccessTo('files')">Files</a> <a href="photos" *ngIf="hasAccessTo('photos')">Photos</a> </ng-container>
虽然在Angular Template 上调用函数非常方便,但是它们会导致严重的性能问题。
接下来将解释性能问题的原因以及如何解决。
问题
假设我们有一个PersonComponent , 使用fullName 函数来显示传入人员的全名:
@Component({ selector: ‘app-person’, template: ` <p>Welcome {{ fullName() }}!</p> <button (click)="onClick()">Trigger change detection</button> ` }) export class PersonComponent { @Input() person: { firstName: string, lastName: string }; constructor() { } fullName() { return this.person.firstName + ' ' + this.person.lastName } onClick() { console.log('Button was clicked'); } }
此时,fullName 函数每次都会在Angular 变化检测时被调用,而且是很多次。而且每次在点击 Person 组件的按钮时fullName 函数都会被执行。
虽然只有点击按钮才会触发fullName 函数执行看起来并没有损耗多少性能,但当需求改变的时候比如:
@Component({ selector: ‘app-person’, template: ` <p> Welcome {{ fullName() }}!</p> <div (mousemove)=”onMouseMove()”> Trigger change detection in mouse move </div> <button (click)=”onClick()”> Trigger change detection </button> ` }) export class PersonComponent { @Input() person: { firstName: string, lastName: string }; constructor() { } fullName() { return this.person.firstName + ’ ’ + this.person.lastName } onMouseMove() { console.log(‘mouse move‘); } onClick() { console.log(‘button was clicked’); } }
当鼠标移动到 Trigger change detection in mouse move 上,fullName 函数会被执行数百次。而且由于 fullName 函数是几个月前编写的,我们可能意识不到对新代码的影响。
此外,当父组件发生变更检测时也会使得PersonComponent 的 Template 执行fullName函数
<app-person [person]=’person’></app-person> <button (click)=”onClick()”> Trigger change detection outside of PersonComponent </button>
每次点击父组件的按钮时都会在PersonComponent 内部执行fullName函数。
为什么Angular Template 中函数会被调用多次?
首先要说一下Angular 的变更检测(Change Detection)
Angular 变更检测的目标是在发生变化时,找出用户界面的哪些部分需要重新渲染。
为了确定<p>Welcome {{ fullName() }}! </p> 是否需要重新渲染,Angular 需要执行fullName() 来检查其返回值是否发生了变化。所以,变更检测运行了300次则fullName 函数就会被调用300次,即使返回值是一样的。这数百次运行函数就可能导致严重的性能问题。
如果我们使用getter 时,性能问题就变得很隐蔽:
@Component({ template: ` <p> Welcome {{ fullName}} !</p> ` }) export class PersonComponent { @Input() person: { firstName: string, lastName: stirng }; constructor() { } get fullName() { return this.person.firstName + ‘ ’ + this.person.lastName; } }
此时, Angular Template 里没有任何函数的踪迹,但是 get fullName() 每次在变更检测时都会被调用。
如果使用变更检测的OnPush 策略会怎样?
比如我们的PersonComponent使用OnPush 策略
@Component({ template: `<p> Welcome {{ fullName}} !</p>`, changeDetection: ChangeDetectionStrategy.OnPush }) export class PersonComponent { @Input() person: { firstName: string, lastName: stirng }; constructor() { } get fullName() { return this.person.firstName + ‘ ’ + this.person.lastName; } }
在PersonComponent 的父组件:
<app-person [person]=”person”></app-person> <button (click)=”onClick”> Trigger change detection outside of PersonCompoent </button>
当点击父组件的button时,PersonComponent 的 fullName() 方法不会再执行。
因为在OnPush 策略下,变更检测会在第一次检查后被禁用,当输入属性(@Input)没有发生改变时,Angualr会跳过OnPush 控件以及其子控件。只有当输入数据(@Input)或者DOM事件被触发组件才进行变更检测。
但是,这并没有解决潜在的性能问题。因为每次Person Component 内部发生变更检测时, fullName 仍然会被执行:
@Component({ template: `<p> Welcome {{ fullName}} !</p> <div (mousemove)=”onMouseMove()”> trigger in Person Component</div> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class PersonComponent { @Input() person: { firstName: string, lastName: stirng }; constructor() { } get fullName() { return this.person.firstName + ‘ ’ + this.person.lastName; } onMouseMove() { console.log(‘mouse move’); } }
如上,每次鼠标移动到trigger in Person Component 上时,仍会执行很多次的fullName
那么如何避免不必要的函数调用呢?
1 Angular Pure Pipe
Angular Pipe 分为pure pipe 和 impure pipe,对于pure pipe 而言只有在参数改变时被触发,而对于impure pipe, 当基本类型参数改变或者引用类型内部发生变化都可以触发。
对于上边的例子:
import {Pipe , PipeTransform} from ‘@angular/core’; @Pipe({ name: ‘fullName’, pure: true }) export class FullNamePipe implements PipeTransform { transform(person: {firstName: string, lastName: string}, args?: any): string { Return person.firstName + ‘ ’ + person.lastName; } }
在我们PersonComponent 里使用:
@Component({ template: `<p> Welcome {{ person | fullName }} !</p> <div (mousemove)=”onMouseMove()”> trigger in Person Component</div> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class PersonComponent { @Input() person: { firstName: string, lastName: stirng }; constructor() { } get fullName() { return this.person.firstName + ‘ ’ + this.person.lastName; } onMouseMove() { console.log(‘mouse move’); } }
因为person 参数没有改变,所以Angular 就跳过Pipe 里的transform 方法。
2 手动计算值
避免不必要的函数调用,我们可以自己在PersonComponent 组件手动计算所需要的值。因为我们能够知道值是何时改变的。
在上边的示例中,我们可以在person被更改时计算全名,因此我们可以添加一个fullName属性,当ngOnChanges 中改变person输入的值时,重新计算fullName的值。
@Component({ template: `<p> Welcome {{ fullName }} !</p> <div (mousemove)=”onMouseMove()”> trigger in Person Component</div> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class PersonComponent implements OnChanges { @Input() person: { firstName: string, lastName: stirng }; constructor() { } onMouseMove() { console.log(‘mouse move’); } ngOnChanges(changes: SimpleChanges) { if (changes.person) { this.fullName = this.calculateFullName(); } } calculateFullName() { return this.person.firstName + ‘ ’ this.person.lastName; } }
因此只有当组件的person 输入发生改变时才会重新计算fullName.