Angular进阶之七: 不要在html template中使用函数(翻译)

为什么绝不应在 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.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值