• 技术文章 >web前端 >js教程

    快速了解Angular中的onPush变更检测策略

    青灯夜游青灯夜游2021-09-09 19:44:36转载141
    本篇文章带大家深入了解一下Angular中的onPush变更检测策略,希望对大家有所帮助!

    默认的变更检测策略

    默认情况下,Angular使用ChangeDetectionStrategy.Default策略来进行变更检测。

    默认策略并不事先对应用做出任何假设,因此,每当用户事件、记时器、XHR、promise等事件使应用中的数据将发生了改变时,所有的组件中都会执行变更检测。

    这意味着从点击事件到从ajax调用接收到的数据之类的任何事件都会触发更改检测。

    通过在组件中定义一个getter并且在模板中使用它,我们可以很容易的看出这一点:

    @Component({
      template: `
        <h1>Hello {{name}}!</h1>
        {{runChangeDetection}}
      `
    })
    export class HelloComponent {
      @Input() name: string;
    
      get runChangeDetection() {
        console.log('Checking the view');
        return true;
      }
    }
    @Component({
      template: `
        <hello></hello>
        <button (click)="onClick()">Trigger change detection</button>
      `
    })
    export class AppComponent  {
      onClick() {}
    }

    执行以上代码后,每当我们点击按钮时。Angular将会执行一遍变更检测循环,在console里我们可以看到两行“Checking the view”的日志。

    这种技术被称作脏检查。为了知道视图是否需要更新,Angular需要访问新值并和旧值比较来判断是否需要更新视图。

    现在想象一下,如果有一个有成千上万个表达式的大应用,Angular去检查每一个表达式,我们可能会遇到性能上的问题。

    那么有没有办法让我们主动告诉Angular什么时候去检查我们的组件呢?

    OnPush变更检测策略

    我们可以将组件的ChangeDetectionStrategy设置成ChangeDetectionStrategy.OnPush

    这将告诉Angular该组件仅仅依赖于它的@inputs(),只有在以下几种情况才需要检查:

    1. Input引用发生改变

    通过设置onPush变更检测测策略,我们与Angular约定强制使用不可变对象(或稍后将要介绍的observables)。

    在变更检测的上下文中使用不可变对象的好处是,Angular可以通过检查引用是否发生了改变来判断视图是否需要检查。这将会比深度检查要容易很多。

    让我们试试来修改一个对象然后看看结果。

    @Component({
      selector: 'tooltip',
      template: `
        <h1>{{config.position}}</h1>
        {{runChangeDetection}}
      `,
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class TooltipComponent  {
    
      @Input() config;
    
      get runChangeDetection() {
        console.log('Checking the view');
        return true;
      }
    }
    @Component({
      template: `
        <tooltip [config]="config"></tooltip>
      `
    })
    export class AppComponent  {
      config = {
        position: 'top'
      };
    
      onClick() {
        this.config.position = 'bottom';
      }
    }

    这时候去点击按钮时看不到任何日志了,这是因为Angular将旧值和新值的引用进行比较,类似于:

    /** Returns false in our case */
    if( oldValue !== newValue ) { 
      runChangeDetection();
    }

    值得一提的是numbers, booleans, strings, null 、undefined都是原始类型。所有的原始类型都是按值传递的. Objects, arrays, 还有 functions 也是按值传递的,只不过值是引用地址的副本

    所以为了触发对该组件的变更检测,我们需要更改这个object的引用。

    @Component({
      template: `
        <tooltip [config]="config"></tooltip>
      `
    })
    export class AppComponent  {
      config = {
        position: 'top'
      };
    
      onClick() {
        this.config = {
          position: 'bottom'
        }
      }
    }

    将对象引用改变后,我们将看到视图已被检查,新值被展示出来。

    2.源于该组件或其子组件的事件

    当在一个组件或者其子组件中触发了某一个事件时,这个组件的内部状态会更新。 例如:

    @Component({
      template: `
        <button (click)="add()">Add</button>
        {{count}}
      `,
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class CounterComponent {
      count = 0;
    
      add() {
        this.count++;
      }
    
    }

    当我们点击按钮时,Angular执行变更检测循环并更新视图。

    你可能会想,按照我们开头讲述的那样,每一次异步的API都会触发变更检测,但是并不是这样。

    你会发现这个规则只适用于DOM事件,下面这些API并不会触发变更检测:

    @Component({
      template: `...`,
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class CounterComponent {
      count = 0;
    
      constructor() {
        setTimeout(() => this.count = 5, 0);
    
        setInterval(() => this.count = 5, 100);
    
        Promise.resolve().then(() => this.count = 5); 
        
        this.http.get('https://count.com').subscribe(res => {
          this.count = res;
        });
      }
    
      add() {
        this.count++;
      }

    注意你仍然是更新了该属性的,所以在下一个变更检测流程中,比如去点击按钮,count值将会变成6(5+1)。

    3. 显示的去执行变更检测

    Angular给我们提供了3种方法来触发变更检测。

    第一个是detectChanges()来告诉Angular在该组件和它的子组件中去执行变更检测。

    @Component({
      selector: 'counter',
      template: `{{count}}`,
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class CounterComponent { 
      count = 0;
    
      constructor(private cdr: ChangeDetectorRef) {
    
        setTimeout(() => {
          this.count = 5;
          this.cdr.detectChanges();
        }, 1000);
    
      }
    
    }

    第二个是ApplicationRef.tick(),它告诉Angular来对整个应用程序执行变更检测。

    tick() {
     
      try {
        this._views.forEach((view) => view.detectChanges());
        ...
      } catch (e) {
        ...
      }
    }

    第三是markForCheck(),它不会触发变更检测。相反,它会将所有设置了onPush的祖先标记,在当前或者下一次变更检测循环中检测。

    markForCheck(): void { 
      markParentViewsForCheck(this._view); 
    }
    
    export function markParentViewsForCheck(view: ViewData) {
      let currView: ViewData|null = view;
      while (currView) {
        if (currView.def.flags & ViewFlags.OnPush) {
          currView.state |= ViewState.ChecksEnabled;
        }
        currView = currView.viewContainerParent || currView.parent;
      }
    }

    需要注意的是,手动执行变更检测并不是一种“hack”,这是Angular有意的设计并且是非常合理的行为(当然,是在合理的场景下)。

    Angular Async pipe

    async pipe会订阅一个 Observable 或 Promise,并返回它发出的最近一个值。

    让我们看一个input()是observable的onPush组件。

    @Component({
      template: `
        <button (click)="add()">Add</button>
        <app-list [items$]="items$"></app-list>
      `
    })
    export class AppComponent {
      items = [];
      items$ = new BehaviorSubject(this.items);
    
      add() {
        this.items.push({ title: Math.random() })
        this.items$.next(this.items);
      }
    }
    @Component({
      template: `
         <div *ngFor="let item of _items ; ">{{item.title}}</div>
      `,
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class ListComponent implements OnInit {
      @Input() items: Observable<Item>;
      _items: Item[];
      
      ngOnInit() {
        this.items.subscribe(items => {
          this._items = items;
        });
      }
    
    }

    当我们点击按钮并不能看到视图更新。这是因为上述提到的几种情况均未发生,所以Angular在当前变更检测循环并不会检车该组件。

    现在,让我们加上async pipe试试。

    @Component({
      template: `
        <div *ngFor="let item of items | async">{{item.title}}</div>
      `,
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class ListComponent implements OnInit {
      @Input() items;
    }

    现在可以看到当我们点击按钮时,视图也更新了。原因是当新的值被发射出来时,async pipe将该组件标记为发生了更改需要检查。我们可以在源码中看到:

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

    Angular为我们调用markForCheck(),所以我们能看到视图更新了即使input的引用没有发生改变。

    如果一个组件仅仅依赖于它的input属性,并且input属性是observable,那么这个组件只有在它的input属性发射一个事件的时候才会发生改变。

    Quick tip:对外部暴露你的subject是不值得提倡的,总是使用asObservable()方法来暴露该observable。

    onPush和视图查询

    @Component({
      selector: 'app-tabs',
      template: `<ng-content></ng-content>`
    })
    export class TabsComponent implements OnInit {
      @ContentChild(TabComponent) tab: TabComponent;
    
      ngAfterContentInit() {
        setTimeout(() => {
          this.tab.content = 'Content'; 
        }, 3000);
      }
    }
    @Component({
      selector: 'app-tab',
      template: `{{content}}`,
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class TabComponent {
      @Input() content;
    }
    <app-tabs>
      <app-tab></app-tab>
    </app-tabs>

    也许你会以为3秒后Angular将会使用新的内容更新tab组件。

    毕竟,我们更新来onPush组件的input引用,这将会触发变更检测不是吗?

    然而,在这种情况下,它并不生效。Angular不知道我们正在更新tab组件的input属性,在模板中定义input()是让Angular知道应在变更检测循环中检查此属性的唯一途径。

    例如:

    <app-tabs>
      <app-tab [content]="content"></app-tab>
    </app-tabs>

    因为当我们明确的在模板中定义了input(),Angular会创建一个叫updateRenderer()的方法,它会在每个变更检测循环中都对content的值进行追踪。

    AppComponent.ngfactory.ts

    在这种情况下简单的解决办法使用setter然后调用markForCheck()

    @Component({
      selector: 'app-tab',
      template: `
        {{_content}}
      `,
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class TabComponent {
      _content;
    
      @Input() set content(value) {
        this._content = value;
        this.cdr.markForCheck();
      }
    
      constructor(private cdr: ChangeDetectorRef) {}
    
    }

    === onPush++

    在理解了onPush的强大之后,我们来利用它创造一个更高性能的应用。onPush组件越多,Angular需要执行的检查就越少。让我们看看你一个真是的例子:

    我们又一个todos组件,它有一个todos作为input()。

    @Component({
      selector: 'app-todos',
      template: `
         <div *ngFor="let todo of todos">
           {{todo.title}} - {{runChangeDetection}}
         </div>
      `,
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class TodosComponent {
      @Input() todos;
    
      get runChangeDetection() {
        console.log('TodosComponent - Checking the view');
        return true;
      }
    
    }
    @Component({
      template: `
        <button (click)="add()">Add</button>
        <app-todos [todos]="todos"></app-todos>
      `
    })
    export class AppComponent {
      todos = [{ title: 'One' }, { title: 'Two' }];
    
      add() {
        this.todos = [...this.todos, { title: 'Three' }];
      }
    }

    上述方法的缺点是,当我们单击添加按钮时,即使之前的数据没有任何更改,Angular也需要检查每个todo。因此第一次单击后,控制台中将显示三个日志。

    在上面的示例中,只有一个表达式需要检查,但是想象一下如果是一个有多个绑定(ngIf,ngClass,表达式等)的真实组件,这将会非常耗性能。

    我们白白的执行了变更检测!

    更高效的方法是创建一个todo组件并将其变更检测策略定义为onPush。例如:

    @Component({
      selector: 'app-todos',
      template: `
        <app-todo [todo]="todo" *ngFor="let todo of todos"></app-todo>
      `,
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class TodosComponent {
      @Input() todos;
    }
    
    @Component({
      selector: 'app-todo',
      template: `{{todo.title}} {{runChangeDetection}}`,
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class TodoComponent {
      @Input() todo;
    
      get runChangeDetection() {
        console.log('TodoComponent - Checking the view');
        return true;
      }
    
    }

    现在,当我们单击添加按钮时,控制台中只会看到一个日志,因为其他的todo组件的input均未更改,因此不会去检查其视图。

    并且,通过创建更小粒度的组件,我们的代码变得更具可读性和可重用性。

    原文链接: https://netbasal.com/a-comprehensive-guide-to-angular-onpush-change-detection-strategy-5bac493074a4

    原文作者:Netanel Basal

    译者:淼淼

    更多编程相关知识,请访问:编程视频!!

    以上就是快速了解Angular中的onPush变更检测策略的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:掘金社区,如有侵犯,请联系admin@php.cn删除
    上一篇:JavaScript中怎么缩小图片 下一篇:深入了解Node.js中的异步编程,分享四种解决方案
    线上培训班

    相关文章推荐

    • 浅谈Angular中如何使用路由?• 深入了解AngularJS中的模块化和依赖注入• 深入了解Angularjs中的视图和指令• 聊聊Angular中的自定义管道

    全部评论我要评论

  • 取消发布评论发送
  • 1/1

    PHP中文网