angular变化检测策略

ngDoCheck 的执行时机

  1. 在状态发生变化,angular 自己本身不能捕获这个变化时会触发 NgDoCheck。

  2. 每次变化检测以后,都会触发 ngDoCheck 钩子函数,紧跟在 ngOnChanges 和 ngOnInit 之后运行。

在这种情况下没有@Input 绑定,所以 ngOnChanges 不会被触发,那为什么组件 B 和 C 的 ngDoCheck 分别执行了两遍

对于 ngOnChanges 生命周期:

当 Angular(重新)设置数据绑定输入属性时响应。 该方法接受当前和上一属性值的 SimpleChanges 对象

在 ngOnInit() 之前以及所绑定的一个或多个输入属性的值发生变化时都会调用。

在父组件执行变化检测,会更新子组件的绑定,从而触发一次子组件的 ngDoCheck;第二遍是子组件它本身的变化检测触发的。

A 组件包含 B 组件;B 组件包含 C 组件,他们的检测过程如下

Checking A component:
  - update B input bindings
  - call NgDoCheck on the B component
  - update DOM interpolations for component A

 Checking B component:
    - update C input bindings
    - call NgDoCheck on the C component
    - update DOM interpolations for component B

   Checking C component:
      - update DOM interpolations for component C
  • 设置组件的检测策略是 OnPush
import { Component, OnChanges, DoCheck, ChangeDetectionStrategy } from "@angular/core";
@Component({
    selector: 'app-componentb',
    template: `<h3>this is component b</h3>
               <app-componentc></app-componentc>`,
    changeDetection: ChangeDetectionStrategy.OnPush
})

将组件 B 设置为 OnPush 策略
此时的检测规则:
没有@Input 绑定、没有 DOM 事件、没有 Observable、没有手动触发变化检测,那么 angular 只会对组件 A 执行变化检测,跳过组件 B 和 C 的变化检测

但是结果却是:
很奇怪对不对!!本来说好的组件 B 和 C 不会执行变化检测,怎么 NgDoCheck 还是触发了?组件 B 中的 ngDoCheck 执行了两遍!!

B check

C check

B check

B 组件设置了 OnPush 时,此时 angular 的检测策略

Checking A component:
  - update B input bindings
  - call NgDoCheck on the B component
  - update DOM interpolations for component A
 if (bindings reference changed
    || DOM event
    || Observable Async pipe
    || ChangeDetectorRef.detectChanges()
    || ChangeDetectorRef.markForCheck()
    || ApplicationRef.tick()) -> checking B component:
    - update C input bindings
    - call NgDoCheck on the C component
    - update DOM interpolations for component B

   Checking C component:
      - update DOM interpolations for component C

解释:

组件 B 执行第一个 NgDoCheck 很好理解:子组件里的 NgDoCheck 每次都是在父组件执行变化检测的时候执行,组件 B 虽然设置了 OnPush 策略,但是父组件 A 的在执行变化检测时会触发一次 B 的 NgDoCheck。

组件 B 执行第二个 NgDoCheck 是因为:组件 C 在它自己的 NgDoCheck 之后会 update DOM interpolations for component B,组件 B 状态发生改变,因为此时组件 B 设置了 OnPush,不会执行变化检测,angular 捕获不到这个状态改变。NgDoCheck 会在状态发生变化,angular 自己又不能捕获时被触发。

组件 C 执行 NgDoCheck 触发的原因和组件 B 的第二个 NgOnCheck 类似:update DOM interpolations for component C 组件 C 本身自己页面渲染时状态发生改变,因为组件 B 被设置为 OnPush 策略,子组件 C 也是 OnPush 策略,angular 捕获不到这个状态改变。NgDoCheck 会在状态发生变化,angular 自己又不能捕获时被触发。

  • 这种情况就解释了 NgDoCheck 执行的第一种情景:在状态发生变化,angular 自己本身不能捕获这个变化时会触发 NgDoCheck

angular 的变更检测策略

angular 默认的变化检测机制是 ChangeDetectionStrategy.Default:异步事件 callback 结束后,NgZone 会触发整个组件树至上而下做变化检测

  • OnPush 策略

angular 除了默认的变化检测机制,也提供了 ChangeDetectionStrategy.OnPush,用 OnPush 可以跳过某个 component 或者某个父组件以及它下面所有子组件的变化检测

在组件中加了 OnPush 表示,在发生异步事件以后触发变化检测,angular 会跳过这个组件,不会触发这个组件的变化检测。如果 OnPush 是加在某个父组件上,那么这个父组件和它下面所有的子组件都不会触发变化检测

某一个组件中设置 angular 变化检测策略为 OnPush,如果没有以下四种情况,angular 是不会为这个组件或者它的子组件执行变化检测

  1. 组件的@Input()引用发生变化。

  2. 组件的 DOM 事件,包括它子组件的 DOM 事件,比如 click、submit、mouse down 等事件。

  3. Observable 订阅事件,同时设置 Async pipe。

  4. ChangeDetectorRef.detectChanges()、ChangeDetectorRef.markForCheck()、ApplicationRef.tick(),手动调用这三种方式触发变化检测。

  • @Input 的属性发生改变

必须是@Input 的引用发生改变才会触发变化检测,并且仅限于@Input 的变化检测,在 OnPush 策略下,会触发组件的变化检测。

  • 组件的 DOM 事件

组件的 DOM 事件,包括它子组件的 DOM 事件,比如 click、submit、mouse down 等事件,在 OnPush 策略下,会触发组件的变化检测。

  • ChangeDetectorRef.detectChanges()

在值改变的地方调用,进行检测

eg:

constructor(private cd: ChangeDetectorRef) { }
    ngOnInit() {
        setInterval(() => {
            this.counter = this.counter + 5;
            this.cd.detectChanges();
        }, 1000);
    }
  • ChangeDetectorRef.markForCheck()

在值改变的地方调用,进行检测

constructor(private cd: ChangeDetectorRef) { }
ngOnInit() {
        setInterval(() => {
            this.counter = this.counter + 10;
            this.cd.markForCheck();
        }, 1000);
    }

效果跟 detectChanges 是一样的,只不过 detectChanges 会立马触发当前组件和它子组件变化检测。markForCheck 并不会立马触发变化检测,而是标记需要被变化检测,在当前或下一轮的变化检测中被触发。

  • ApplicationRef.tick()
constructor(private applicationRef: ApplicationRef)) { }

    ngOnInit() {
        setInterval(() => {
            this.counter = this.counter + 20;
            this.applicationRef.tick();
        }, 1000);
    }

ApplicationRef.tick()触发整个应用的组件树从上到下执行变化检测。

  • 总结:

在实际应用开发过程中,应该尽量遵循以下的原则:

  1. 尽量使用 OnPush 策略从叶节点组件开始来优化整个应用的性能

  2. 尽量多结合使用 OnPush 和 async pipe

angular 变化检测机制

变化检测都是沿着组件树从 root component 开始至上而下执行的。

我们都知道在 angular 里,每个 component 都有一个 html 模板,在 angular 内部,编译器在 component 和模板之间会生成一个 component view。数据绑定、脏数据检查和更新 DOM 都是由这个 component view 实现的。变化检测机制也可以说就是沿着 component view 的树状结构从上到下执行的。

angular 需要在 component view 保存每个 DOM 节点引用,同时也要保存 component 数据引用、数据之前的值和取值表达式

  • angular 通常有如下三种方式会导致组件数据变化:
  1. 事件:页面 click、submit、mouse down……

  2. XHR:从后端服务器拿到数据

  3. Timers:setTimeout()、setInterval()

  • 前面那三种方式会导致 angular 状态变化,那又是谁知道状态已经发生改变,需要通知 angular 触发变化检测从而更新页面 DOM 呢?NgZone(zone.js)充当了这个角色。

NgZone 可以简单的理解为是一个异步事件拦截器,它能够 hook 到异步任务的执行上下文,然后就可以来处理一些操作,比如每个异步任务 callback 以后就会去通知 angular 做变化检测

angular 源码中有一个 ApplicationRef,可以监听 NgZones onTurnDone 事件,每当 onTurnDone 被触发后,它会立马执行 tick()方法,tick()会从上到下沿着组件树触发变化检测。ApplicationRef 简洁版代码如下

// very simplified version of actual source
class ApplicationRef {
  changeDetectorRefs:ChangeDetectorRef[] = [];

  constructor(private zone: NgZone) {
    this.zone.onTurnDone
      .subscribe(() => this.zone.run(() => this.tick());
  }

  tick() {
    this.changeDetectorRefs
      .forEach((ref) => ref.detectChanges());
  }
}

有了 NgZone 上述三种异步事件都会导致整个 angular 应用发生变化检测,虽然 angular 变化检测本身性能已经很好了,在毫秒内可以做成百上千次变化检测。但是随着项目越来越大,其实很多不必要的变化检测还是会在一定程度上影响性能.解决方式 OnPush 策略

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容