0

0

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

青灯夜游

青灯夜游

发布时间:2021-09-09 19:44:36

|

2484人浏览过

|

来源于掘金社区

转载

本篇文章带大家深入了解一下angular中的onpush变更检测策略,希望对大家有所帮助!

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

默认的变更检测策略

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

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

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

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

@Component({
  template: `
    

Hello {{name}}!

{{runChangeDetection}} ` }) export class HelloComponent { @Input() name: string; get runChangeDetection() { console.log('Checking the view'); return true; } }
@Component({
  template: `
    
    
  `
})
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: `
    

{{config.position}}

{{runChangeDetection}} `, changeDetection: ChangeDetectionStrategy.OnPush }) export class TooltipComponent { @Input() config; get runChangeDetection() { console.log('Checking the view'); return true; } }
@Component({
  template: `
    
  `
})
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: `
    
  `
})
export class AppComponent  {
  config = {
    position: 'top'
  };

  onClick() {
    this.config = {
      position: 'bottom'
    }
  }
}

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

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

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

@Component({
  template: `
    
    {{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有意的设计并且是非常合理的行为(当然,是在合理的场景下)。

DALL·E 2
DALL·E 2

OpenAI基于GPT-3模型开发的AI绘图生成工具,可以根据自然语言的描述创建逼真的图像和艺术。

下载

Angular Async  pipe

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

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

@Component({
  template: `
    
    
  `
})
export class AppComponent {
  items = [];
  items$ = new BehaviorSubject(this.items);

  add() {
    this.items.push({ title: Math.random() })
    this.items$.next(this.items);
  }
}
@Component({
  template: `
     
{{item.title}}
`, changeDetection: ChangeDetectionStrategy.OnPush }) export class ListComponent implements OnInit { @Input() items: Observable; _items: Item[]; ngOnInit() { this.items.subscribe(items => { this._items = items; }); } }

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

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

@Component({
  template: `
    
{{item.title}}
`, 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: ``
})
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;
}

  

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

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

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

例如:


  

因为当我们明确的在模板中定义了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: `
     
{{todo.title}} - {{runChangeDetection}}
`, changeDetection: ChangeDetectionStrategy.OnPush }) export class TodosComponent { @Input() todos; get runChangeDetection() { console.log('TodosComponent - Checking the view'); return true; } }
@Component({
  template: `
    
    
  `
})
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: `
    
  `,
  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

译者:淼淼

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

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
ajax教程
ajax教程

php中文网为大家带来ajax教程合集,Ajax是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,Ajax可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。php中文网还为大家带来ajax的相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

157

2023.06.14

ajax中文乱码解决方法
ajax中文乱码解决方法

ajax中文乱码解决方法有设置请求头部的字符编码、在服务器端设置响应头部的字符编码和使用encodeURIComponent对中文进行编码。本专题为大家提供ajax中文乱码相关的文章、下载、课程内容,供大家免费下载体验。

160

2023.08.31

ajax传递中文乱码怎么办
ajax传递中文乱码怎么办

ajax传递中文乱码的解决办法:1、设置统一的编码方式;2、服务器端编码;3、客户端解码;4、设置HTTP响应头;5、使用JSON格式。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

116

2023.11.15

ajax网站有哪些
ajax网站有哪些

使用ajax的网站有谷歌、维基百科、脸书、纽约时报、亚马逊、stackoverflow、twitter、hacker news、shopify和basecamp等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

231

2024.09.24

c语言中null和NULL的区别
c语言中null和NULL的区别

c语言中null和NULL的区别是:null是C语言中的一个宏定义,通常用来表示一个空指针,可以用于初始化指针变量,或者在条件语句中判断指针是否为空;NULL是C语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

235

2023.09.22

java中null的用法
java中null的用法

在Java中,null表示一个引用类型的变量不指向任何对象。可以将null赋值给任何引用类型的变量,包括类、接口、数组、字符串等。想了解更多null的相关内容,可以阅读本专题下面的文章。

437

2024.03.01

counta和count的区别
counta和count的区别

Count函数用于计算指定范围内数字的个数,而CountA函数用于计算指定范围内非空单元格的个数。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

198

2023.11.20

java值传递和引用传递有什么区别
java值传递和引用传递有什么区别

java值传递和引用传递的区别:1、基本数据类型的传递;2、对象的传递;3、修改引用指向的情况。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

108

2024.02.23

拼多多赚钱的5种方法 拼多多赚钱的5种方法
拼多多赚钱的5种方法 拼多多赚钱的5种方法

在拼多多上赚钱主要可以通过无货源模式一件代发、精细化运营特色店铺、参与官方高流量活动、利用拼团机制社交裂变,以及成为多多进宝推广员这5种方法实现。核心策略在于通过低成本、高效率的供应链管理与营销,利用平台社交电商红利实现盈利。

12

2026.01.26

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Vue.js:纪录片
Vue.js:纪录片

共1课时 | 0.2万人学习

Angular js入门篇
Angular js入门篇

共17课时 | 3.5万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号