angular 检测更新

视图View

1.视图与组件的关系(View && Component)

  • 一个视图对应一个组件
  • 一个组件对应一个视图
  • 视图引用组件实例
  • 所有操作(属性检测,DOM更新)都发生在视图上,所以与其说angular是组件树,不如说其是视图树
  • 组件可以用来描述视图的高级概念

2.视图与视图容器即子视图

  • 一个视图上的元素的属性可以改变,但是在视图中的元素的结构(数量和顺序)不能够改变。改变元素结构的唯一方式是,通过 ViewContainerRef 进行 插入,移动或者移除嵌套的Views
  • 一个视图能够包含多个视图容器(View Containers)
  • 每个视图都能够通过 nodes属性与其子视图建立联系,因此能够对其子视图进行操作

3.视图状态(View State)

每个视图都有一个 state,这个状态很重要,因为angular会根据它的值来决定对视图及其子视图是否进行检测还是直接跳过检测。

与状态变化检测相关的可能的状态flags有:

  • FirstCheck
  • ChecksEnabled
  • Errored
  • Destroyed
    这些状态是可以混合设置的,比如同时设置 FirstCheckChecksEnabled flags

默认情形下,所有的视图都是开启 ChecksEnabled flag的,除非使用 ChangeDetectionStrategy.OnPush 策略。

跳过检测的情形有:

  1. ChecksEnabled 设置为了 false
  2. 视图进入 Errored 状态
  3. 视图进入 Destroyed 状态

4.ViewRef

angular有很多操作视图的高级概念,其中一个就是 ViewRef,它封装了底层的组件视图,并且有个直观的方法叫 detectChanges(检测变化)。

当发生异步事件时,angular将从最上层的ViewRef触发变化检测,检测完后再检测其子视图

ViewRef 可以通过 ChangeDetectorRef token 注入到组件构造器中

export class MyComponent {
  constructor(cd: ChangeDetectorRef) {}
}

ViewRef 的定义

export declare abstract class ChangeDetectorRef {
  abstract checkNoChanges(): void;
  abstract detach(): void;
  abstract detectChanges(): void;
  abstract markForCheck(): void;
  abstract reattach(): void;
}

export abstract class ViewRef extends ChangeDetectorRef {
  // ...
}

上面几个概念:

1.底层的组件视图

2.detectChange方法

class ViewRef_ implements EmbeddedViewRef<any>, InternalViewRef {
  // 内部属性
  // 1.底层的组件视图
  _view: ViewData;
  
  // ...
  
  // 2.检测变化方法
  detectChanges(): void { Services.checkAndUpdateView(this._view); }
}

3.触发检测

class ApplicationRef_ extends ApplicationRef {
  // ...
  
  // 3.触发检测
  tick(): void {
    // ...
    
    try {
        this._views.forEach((view) => view.detectChanges());
    }
  }
}

变化检测操作

运行视图变化检测的主要逻辑都在 checkAndUpdateView这个函数中。

这个函数将从宿主组件开始调用,完成之后再到其子组件中调用,依次递归,直到组件树调用完成。

当这个方法发生在某个特定视图时,会按顺序出现以下步骤:

  1. 如果视图是第一次被检测,将会设置 ViewState.firstChecktrue, 如果不是第一次检测,这个flag将设置为 false
  2. 检测和更新子组件或者子指令的输入属性input properties
  3. 更新子视图变化检测状态(这是变化检测策略实现的一部分)
  4. 对插入的视图运行变化检测 (重复list中的步骤)
  5. 如果子组件的绑定值发生变化,将调用 OnChanges 生命周期函数
  6. 调用 OnInitngDoCheck 生命周期函数 (OnInit 只在第一次检测的时候的被调用)
  7. 更新子视图组件实例中的 ContentChildren query list, 调用 checkAndUpdateQuery 函数
  8. 子组件实例调用 AfterContentInitAfterContentChecked 生命周期函数 (AfterContentInit 只在第一次检测的时候的被调用), 调用 callProviderLifecycles 函数
  9. 如果当前视图组件实例的属性发生改变,则会更新DOM插值, 可参考 The mechanics of DOM updates in Angular
  10. 对子视图运行变化检测 (重复下面步骤)
  11. 对当前组件实例更新 ViewChildren query list
  12. 对子组件实例调用 AfterViewInitAfterViewChecked 生命周期函数 (AfterViewInit 只在第一次检测的时候的被调用)
  13. 对当前视图禁用检测 (这是变化检测策略实现的一部分)

上面步骤中值得注意的地方

  1. 子组件的 OnChanges 生命周期函数 在 子视图被检测之前调用,即使子视图跳过变化检测步骤,这个钩子也会触发,这个比较重要的点
  2. 当视图检测更新时,DOM将作为变化检测机制的一部分来更新。这也意味着,如果组件没有被检测,即使存在于模板中的组件属性发生了变化,DOM也不会被更新。模板在first check之前被渲染。DOM更新实际上是插值更新。比如 <span>some {{name}}</span>, DOM元素 span在first check之前就已经渲染,在检测时, {{name}} 插值部分将被渲染。
  3. 子组件视图的状态能够在检测变化时发生改变。默认情况下, 所有组件视图的 checksEnabled 是开启的,但是如果组件检测更新策略设置为了 OnPush, 在first check之后检测更新将被禁用.(上面的步骤9)
```
# 对使用了 ChangeDetectionStrategy.OnPush 检测更新策略的组件
# 只有父组件视图绑定值发生改变,子组件才会更新
if (compView.def.flags & ViewFlags.OnPush) {
  compView.state |= ViewState.ChecksEnabled;
}
``` 
  1. 当前视图的变化检测负责开启子视图的变化检测(步骤8)
  2. 一些生命周期钩子在DOM更新(步骤3, 4, 5为DOM更新) 之前被调用, 一些生命周期钩子在DOM更新之后被调用(步骤9)

如果组件层级为 A -> B -> C ,下面就是钩子和绑定更新执行的顺序

A: AfterContentInit
A: AfterContentChecked
A: Update bindings
     B: AfterContentInit
     B: AfterContentChecked
     B: Update bindings
        C: AfterContentInit
        C: AfterContentChecked
        C: Update bindings
        C: AfterViewInit
        C: AfterViewChecked
    B: AfterViewInit
    B: AfterViewChecked
A: AfterViewInit
A: AfterViewChecked

变化检测APIs

ChangeDetectorRef 接口定义

class ChangeDetectorRef {
  markForCheck() : void
  detach() : void
  reattach() : void
  
  detectChanges() : void
  checkNoChanges() : void
}

假设我们要禁用 AComponent 和 其子组件的检测更行,使用下面方法各自的不同

#1 组件树.png

detach

这个方法用来禁用当前视图的检测

detach(): void { this._view.state &= ~ViewState.ChecksEnabled; }

使用

export class AComponent {
  constructor(public cd: ChangeDetectorRef) {
    this.cd.detach();
}

这样A组件和其子组件都将跳过变化检测(橙色部分), 因为跳过了检测,组件模版中的DOM也不会更新

#2 禁用更新检测.png

示例:

@Component({
  selector: 'a-comp',
  template: `<span>See if I change: {{changed}}</span>`
})
export class AComponent {
  constructor(public cd: ChangeDetectorRef) { // 注入ChangeDetectorRef服务 
    this.changed = 'false';

    setTimeout(() => {
      this.cd.detach();            // 禁用变化检测
      this.changed = 'true';
    }, 2000);
  }

组件模版在first check之后渲染结果为 See if I change: false,2秒后, changed 值变化为 true, 但是因为跳过了变化检测,DOM中的插值并不会更新

reattach

设置 ViewState.ChecksEnabled的值

reattach(): void { this._view.state |= ViewState.ChecksEnabled; }

如果AComponent的输入值属性(假设为 name)发生了变化, OnChanges 钩子函数将调用。因此一旦输入属性发生了变化,我们可以在 OnChanges 钩子函数中重新激活变化检测,然后在下一次的时候再detach

// 子组件
@Component({
  selector: 'a-comp',
  template: `<span>See if I change: {{changed}}</span>`
})
export class AComponent {
  @Input() name;  // 输入属性
  constructor(public cd: ChangeDetectorRef) { // 注入ChangeDetectorRef服务 
    this.cd.detach();  // 禁用变化检测
  }
  
  OnChanges(values): void {
    this.cd.reattach();  // 激活变化检测
    
    setTimeout(() => { // 下一次的时候再禁用变化检测
      this.cd.detach();
    })
  }
}  

// 父组件
@Component({
  selector: 'app-comp',
  template: `<a-comp [name]="yourName"></a-comp><button (click)="changeName()">更新name</button>`
})
export class AppComponent {
  yourName = 'james';
  
  changeName() {
    this.yourName = 'kobe';
  }
}

当我们点击 '更新name' 按钮时, AComponent中的输入属性 name 将发生变化,此时会调用OnChanges 钩子函数,从而激活变化检测,因此DOM会产生更新.

上面示例的形式,等同于将组件的更新策略设置为 ChangeDetectionStrategy.OnPush: 在第一次变化检测更新运行之后,禁用检测更新,但父组件绑定属性发生变化时,再开启检测更新,检测更新完成之后再次禁用掉检测更新。

注意:

  • OnChanges只在禁用分支的最顶层组件(此处是AComponent)触发,而不是禁用分支中的所有组件。

markForCheck

reattach 只对当前组件开启检测,但是如果其父组件没有开启更新检测,则还是没有效果。

我们需要一个方法来开启所有父组件中检测更新,这个方法就是 markForCheck

# 可以看出这个方法向上迭代开启检测更新
let currView: ViewData|null = view;
while (currView) {
  if (currView.def.flags & ViewFlags.OnPush) {
    currView.state |= ViewState.ChecksEnabled;
  }
  currView = currView.viewContainerParent || currView.parent;
}

ngOnChangesngDoCheck 这2个钩子函数即使在使用 OnPush 更新策略时也会触发,同样它们只在禁用分支的最顶层触发,而不是所有组件中都会触发。我们可以实现一些自定义逻辑(个人感觉和react的 shouldComponentUpdate 方法很像)。

因为angular只检测对象引用,我们可以实现对象属性的脏值检查

import { Component, ChangeDetectorRef, Input, ChangeDetectionStrategy, OnInit, DoCheck } from '@angular/core';

@Component({
  selector: 'a-comp',
  template: `<span>数量 {{prevLength}}</span>`,
  changeDetection: ChangeDetectionStrategy.OnPush // 更新策略
})
export class AComponent implements OnInit, DoCheck {
  @Input() items;
  prevLength: number;
  constructor(public cd: ChangeDetectorRef) {}

  ngOnInit(): void {
    this.prevLength = this.items.length;
  }

  ngDoCheck() {
    if (this.items.length !== this.prevLength) { // 只有当items数量变化时才更新
      
      this.cd.markForCheck();  // 检测父组件和自身以及子组件
      this.prevLength = this.items.length;
    }
  }

}

# 父组件
import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <button (click)="addItem()">加item</button>
    <a-comp [items]="items"></a-comp>
  `,
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {

  items = ['a', 'b', 'c']


  addItem() {
    this.items.push('d');
  }
}

detectChanges

对当前组件及其子组件都运行检测。即使组件是否检测状态是关闭的,也会运行检测。

感觉这个和angularjs1.x中的 $apply | $digest 方法很像。

export class AComponent {
  @Input() inputAProp;

  constructor(public cd: ChangeDetectorRef) {
    this.cd.detach();
  }

  ngOnChanges(values) {
    this.cd.detectChanges();
}

checkNoChanges

这个用于确保当前更新检测中不发生任何变化,如果绑定属性发生变化或者DOM将更新,都会抛出错误。

基本上走上面 1, 7, 8 步骤。

markForCheck 和 detectChanges 区别

参考:

本文来源:

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • Angular 2架构总览 - 简书http://www.jianshu.com/p/aeb11061b82c A...
    葡萄喃喃呓语阅读 1,478评论 2 13
  • 随着社会的进步,人们的生活质量不断提升,可是人们的保健意识却在不断下降。不少人平时没有好好爱护身体,到出现问题影响...
    媛美人生阅读 465评论 0 0
  • 建立在生意上的友谊远胜于建立在友谊上的生意。往上爬的时候对别人好一点,因为你下坡的时候会碰到他们。
    五心阅读 203评论 0 0