OnPush 组件中 NgDoCheck 和 AsyncPipe 的区别

原文:The difference between NgDoCheck and AsyncPipe in OnPush components
做者:Max Koretskyi
原技术博文由 Max Koretskyi 撰写发布,他目前于 ag-Grid 担任开发大使(Developer Advocate)
译者按:开发大使负责确保其所在的公司认真听取社区的声音并向社区传达他们的行动及目标,其做为社区和公司之间的纽带存在。
译者:Ice Panpan;校对者:vaanxygit

这篇文章是对Shai这条推特的回应。他询问使用 NgDoCheck 生命周期钩子来手动比较值而不是使用 asyncPipe 是否有意义。这是一个很是好的问题,须要对引擎的工做原理有不少了解:变化检测(change detection),管道(pipe)和生命周期钩子(lifecycle hooks)。那就是我探索的入口😎。github

在本文中,我将向您展现如何手动处理变动检测。这些技术使您能够更好地掌控 Angular 的输入绑定(input bindings)的自动执行和异步值检查(async values checks)。掌握了这些知识以后,我还将与您分享我对这些解决方案的性能影响的见解。让咱们开始吧!api

OnPush 组件

在 Angular 中,咱们有一种很是常见的优化技术,须要将 ChangeDetectionStrategy.OnPush 添加到组件中。假设咱们有以下两个简单的组件:app

@Component({
    selector: 'a-comp',
    template: ` <span>I am A component</span> <b-comp></b-comp> `
})
export class AComponent {}

@Component({
    selector: 'b-comp',
    template: `<span>I am B component</span>`
})
export class BComponent {}
复制代码

这样设置以后, Angular 每次都会对 AB 两个组件运行变动检测。若是咱们如今为 B 组件添加上 OnPush 策略:dom

@Component({
    selector: 'b-comp',
    template: `<span>I am B component</span>`,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class BComponent {}
复制代码

只有在输入绑定的值发生变化时 Angular 才会对 B 运行变动检测。因为它如今没有任何绑定,所以该组件只会在初始化的时候检查一次。异步

手动触发变动检测

有没有办法强制对 B 组件进行变动检测?是的,咱们能够注入 changeDetectorRef 并使用它的方法 markForCheck 来指示 Angular 须要检查该组件。而且因为 NgDoCheck 钩子仍然会被 B 组件触发,因此咱们应该在 NgDoCheck 中调用 markForCheckasync

@Component({
    selector: 'b-comp',
    template: `<span>I am B component</span>`,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class BComponent {
    constructor(private cd: ChangeDetectorRef) {}

    ngDoCheck() {
        this.cd.markForCheck();
    }
}
复制代码

如今,当 Angular 检查父组件 A 时,将始终检查 B 组件。如今让咱们看看咱们能够在哪里使用它。函数

输入绑定

我以前说过,Angular 只在 OnPush 组件的绑定发生变化时运行的变化检测。因此让咱们看一下输入绑定的例子。假设咱们有一个经过输入绑定从父组件传递下来的对象:工具

@Component({
    selector: 'b-comp',
    template: ` <span>I am B component</span> <span>User name: {{user.name}}</span> `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class BComponent {
    @Input() user;
}
复制代码

在父组件 A 中,咱们定义了一个对象,并实现了在单击按钮时来更新对象名称的 changeName 方法:性能

@Component({
    selector: 'a-comp',
    template: ` <span>I am A component</span> <button (click)="changeName()">Trigger change detection</button> <b-comp [user]="user"></b-comp> `
})
export class AComponent {
    user = {name: 'A'};

    changeName() {
        this.user.name = 'B';
    }
}
复制代码

若是您如今运行此示例,则在第一次变动检测后,您将看到用户名称被打印出来:

User name: A
复制代码

可是当咱们点击按钮并回调中更更名称时:

changeName() {
    this.user.name = 'B';
}
复制代码

该名称并无在屏幕上更新,这是由于 Angular 对输入参数执行浅比较,而且对 user 对象的引用没有改变。那咱们怎么解决这个问题呢?

好吧,咱们能够在检测到差别时手动检查名称并触发变动检测:

@Component({
    selector: 'b-comp',
    template: ` <span>I am B component</span> <span>User name: {{user.name}}</span> `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class BComponent {
    @Input() user;
    previousName = '';

    constructor(private cd: ChangeDetectorRef) {}

    ngDoCheck() {
        if (this.previousName !== this.user.name) {
            this.previousName = this.user.name;
            this.cd.markForCheck();
        }
    }
}
复制代码

若是您如今运行此代码,你将在屏幕上看到更新的名称。

异步更新

如今,让咱们的例子更复杂一点。咱们将介绍一种基于 RxJs 的服务,它能够异步发出更新。这相似于 NgRx 的体系结构。我将使用一个 BehaviorSubject 做为值的来源,由于咱们须要在这个流的最开始设置初始值:

@Component({
    selector: 'a-comp',
    template: ` <span>I am A component</span> <button (click)="changeName()">Trigger change detection</button> <b-comp [user]="user"></b-comp> `
})
export class AComponent {
    stream = new BehaviorSubject({name: 'A'});
    user = this.stream.asObservable();

    changeName() {
        this.stream.next({name: 'B'});
    }
}
复制代码

因此咱们须要在子组件中订阅这个流并从中获取到 user 对象。咱们须要订阅流并检查值是否更新。这样作的经常使用方法是使用 AsyncPipe

AsyncPipe

因此这里是子组件 B 的实现:

@Component({
    selector: 'b-comp',
    template: ` <span>I am B component</span> <span>User name: {{(user | async).name}}</span> `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class BComponent {
    @Input() user;
}
复制代码

这是演示。可是,还有另外一种不使用管道的方法吗?

手动检查而且变动检测

是的,咱们能够手动检查值并在须要时触发变动检测。正如开头的例子同样,咱们可使用 NgDoCheck 生命周期钩子:

@Component({
    selector: 'b-comp',
    template: ` <span>I am B component</span> <span>User name: {{user.name}}</span> `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class BComponent {
    @Input('user') user$;
    user;
    previousName = '';

    constructor(private cd: ChangeDetectorRef) {}

    ngOnInit() {
        this.user$.subscribe((user) => {
            this.user = user;
        })
    }

    ngDoCheck() {
        if (this.previousName !== this.user.name) {
            this.previousName = this.user.name;
            this.cd.markForCheck();
        }
    }
}
复制代码

你能够在这查看

咱们但愿把值的比较与更新逻辑从 NgDoCheck 中移至订阅的回调函数,由于咱们是从那里获取到新值的:

export class BComponent {
    @Input('user') user$;
    user = {name: null};

    constructor(private cd: ChangeDetectorRef) {}

    ngOnInit() {
        this.user$.subscribe((user) => {
            if (this.user.name !== user.name) {
                this.cd.markForCheck();
                this.user = user;
            }
        })
    }
}
复制代码

例子在这

有趣的是,这其实正是 AsyncPipe 背后的工做原理

@Pipe({name: 'async', pure: false})
export class AsyncPipe implements OnDestroy, PipeTransform {
  constructor(private _ref: ChangeDetectorRef) {}

  transform(obj: ...): any {
    ...
    this._subscribe(obj);

    ...
    if (this._latestValue === this._latestReturnedValue) {
      return this._latestReturnedValue;
    }

    this._latestReturnedValue = this._latestValue;
    return WrappedValue.wrap(this._latestValue);
  }

  private _subscribe(obj): void {
    ...
    this._strategy.createSubscription(
        obj, (value: Object) => this._updateLatestValue(obj, value));
  }

  private _updateLatestValue(async: any, value: Object): void {
    if (async === this._obj) {
      this._latestValue = value;
      this._ref.markForCheck();
    }
  }
}
复制代码

那么那种解决方案更快?

如今咱们知道如何使用手动进行变动检测而不是使用 AsyncPipe,让咱们回答下最一开始的问题。那种方法更快?

嗯...这取决于你如何比较它们,但在其余条件相同的状况下,手动方法会更快。尽管我不认为二者会有明显区别。如下是为何手动方法能够更快的几个例子。

就内存而言,您不须要建立 Pipe 类的实例。就编译时间而言,编译器没必要花时间解析管道特定语法并生成管道特定输出。就运行时间而言,节省了异步管道为组件进行变动检测所调用的函数的时间。这个例子演示了当代码中包含 pipe 时 updateRenderer 所生成的代码:

function (_ck, _v) {
    var _co = _v.component;
    var currVal_0 = jit_unwrapValue_7(_v, 3, 0, asyncpipe.transform(_co.user)).name;
    _ck(_v, 3, 0, currVal_0);
}
复制代码

如您所见,异步管道的代码调用管道实例上的 transform 方法以获取新值。管道将返回从订阅中收到的最新值。

将其与为手动方法生成的普通代码进行比较:

function(_ck,_v) {
    var _co = _v.component;
    var currVal_0 = _co.user.name;
    _ck(_v,3,0,currVal_0);
}
复制代码

这就是 Angular 在检查 B 组件时调用的方法。

一些更有趣的事情

与执行浅比较的输入绑定不一样,异步管道的实现根本不执行比较(感谢 Olena Horal 注意到这一点)。它将每一个新发射的值认为是更新,即便它与先前发射的值同样。下面的代码是父组件 A 的实现,它每次都发射出相同的对象。尽管如此,Angular 仍然会对 B 组件进行变动检测:

export class AComponent {
    o = {name: 'A'};
    user = new BehaviorSubject(this.o);

    changeName() {
        this.user.next(this.o);
    }
}
复制代码

这意味着每次发出新值时,使用异步管道的组件都会被标记以进行检查。而且 Angular 将在下次运行变动检测时检查该组件,即便该值未更改。

这是应用于什么状况呢?嗯...在咱们的例子中,咱们只关注 user 对象的 name 属性,由于咱们须要在模板中使用它。咱们并不关心整个对象以及对象的引用可能会改变的事实。若是 name 没有发生改变,咱们不须要从新渲染组件。但你没法用异步管道来避免这种状况。

NgDoCheck 并非没有问题:)因为仅在检查父组件时触发钩子,若是其中一个父组件使用 OnPush 策略而且在变动检测期间未检查,则不会触发该钩子。所以,当您经过服务收到新值时,不能依赖它来触发变动检测。在这种状况下,我在订阅回调中调用 markForCheck 方法是正确的解决方案。

总结

基本上,手动比较可让您更好地控制检查。您能够定义什么时候须要检查组件。这与许多其余工具相同 - 手动控制为您提供了更大的灵活性,但您必须知道本身在作什么。为了得到这些知识,我鼓励您投入时间和精力学习和阅读更多文章

你不用担忧 NgDoCheck 生命周期钩子被调用的频率,或者它会比管道的 transform 方法更频繁地被调用。首先,我上面已经展现了解决方案,当使用异步流时,你应该在订阅的回调中而非在该钩子函数中手动执行变动检测。其次,只有在父组件被检测后才会调用该钩子函数。若是父组件没有被检查,则不会调用该钩子。对于管道而言,因为流中的浅比较和更改引用的缘由,管道的 transform 方法被调用的次数只会和手动方法相同甚至更多。

想要了解更过关于 Angular 中 change detection 的相关知识?

从这5篇文章入手会让你成为Angular Change Detection 的专家。若是你想要牢固掌握 Angular 中变动检测机制,那么这一系列的文章是必读的。每一篇文章都会基于前一篇文章中所解释的相关信息,既包含高层次的概述又囊括了具体的实现细节,而且都附有相关源代码。

相关文章
相关标签/搜索