Angular中的变更检测

1.什么是变更检测?

变更检测就是Angular检测视图与数据模型之间绑定的值是否发生了改变,当检测到模型中绑定的值发生改变时,就把数据同步到视图上。

2.何时执行变更检测?

我们先看下面这个例子

import {Component, OnInit} from '@angular/core';
import {HttpClient} from '@angular/common/http';

@Component({
  selector: 'app-check-test',
  templateUrl: './check-test.component.html',
  styleUrls: ['./check-test.component.css']
})
export class CheckTestComponent implements OnInit {
  content: string = '究竟是谁老是触发检测?';

  constructor(private http:HttpClient) {
  }

  ngOnInit() {
    setTimeout(()=>{
      this.changeContent();
    },3000)
  }

  changeContent() {
    this.content = '又触发了,id是:'+Math.random();
  }

  xmlRequest(){
    this.http.get('../../assets/testcheck.json').subscribe(
      (res)=>{
        this.changeContent();
      }
    )
  }

}

通过以上例子我们可以总结出来,在异步事件发生的时候可能会使数据模型发生变化。可是angular是如何检测到异步事件发生了呢?这还要说起zone.js。

  • 事件:click, mouseover, keyup ...
  • 定时器:setInterval、setTimeout
  • Http请求:xmlHttpRequest

3.zone.js

官方定义zone.js是javascript的线程本地存储技术,猛地一听感觉好高大上,其实zone.js就是一种用来拦截和跟踪异步工作,为JavaScript提供执行上下文的插件。
那么它是如何感知到异步事件呢,其实方法相当简单粗暴,zone.js采用一种叫做猴子补丁 (Monkey-patched)的方式,将JavaScript中的异步任务都进行了包装,这使得这些异步任务都能运行在Zone(一个全局的对象,用来配置有关如何拦截和跟踪异步回调的规则)的执行上下文中,每个异步任务在 Zone 中都是一个任务(Task),除了提供了一些供开发者使用的钩子外,默认情况下Zone重写了以下方法:

  • setInterval、clearInterval、setTimeout、clearTimeout
  • alert、prompt、confirm
  • requestAnimationFrame、cancelAnimationFrame
  • addEventListener、removeEventListener

zone.js部分源码

var set = 'set';
var clear = 'clear';
var blockingMethods = ['alert', 'prompt', 'confirm'];
var _global = typeof window === 'object' && window ||
    typeof self === 'object' && self || global;
patchTimer(_global, set, clear, 'Timeout');
patchTimer(_global, set, clear, 'Interval');
patchTimer(_global, set, clear, 'Immediate');
patchTimer(_global, 'request', 'cancel', 'AnimationFrame');
patchTimer(_global, 'mozRequest', 'mozCancel', 'AnimationFrame');
patchTimer(_global, 'webkitRequest', 'webkitCancel', 'AnimationFrame');

通过打印window对象我们可以发现zone.js对异步方法进行了封装,非异步方法并没有处理。

0B01D4D7-1CC5-4749-AAAD-5E7523EC93F7.png

zone.js本身比较庞大复杂,这里不做深入研究,对它的原理感兴趣的可以看一下这篇文章-zone.js。我们这里主要是了解它是怎么配合Angular工作的即可。

在 Angular 源码中,有一个 ApplicationRef 类,其作用是当异步事件结束的时候由 onMicrotaskEmpty执行一个 tick 方法 提示 Angular 执行变更检测及更新视图。

interface ApplicationRef {
    get componentTypes: Type<any>[]
    get components: ComponentRef<any>[]
    get isStable: Observable<boolean>
    get viewCount
    bootstrap<C>(componentOrFactory: ComponentFactory<C> | Type<C>, rootSelectorOrNode?: string | any): ComponentRef<C>
    tick(): void
    attachView(viewRef: ViewRef): void
    detachView(viewRef: ViewRef): void
}

调用tick方法。其中this._zone 是NgZone 的一个实例, NgZone 是对zone.js的一个简单封装。

this._zone.onMicrotaskEmpty.subscribe({
  next: () => {
    this._zone.run(() => { this.tick();});
  }
});

tick函数对所有附在 ApplicationRef上的视图进行脏检查。

tick() {
    if (this._runningTick) {
        throw new Error('ApplicationRef.tick is called recursively');
    }
    const /** @type {?} */ scope = ApplicationRef._tickScope();
    try {
        this._runningTick = true;
        this._views.forEach((view) => view.detectChanges());
        if (this._enforceNoNewChanges) {
            this._views.forEach((view) => view.checkNoChanges());
        }
    }
    catch (/** @type {?} */ e) {
        // Attention: Don't rethrow as it could cancel subscriptions to Observables!
        this._zone.runOutsideAngular(() => this._exceptionHandler.handleError(e));
    }
    finally {
        this._runningTick = false;
        wtfLeave(scope);
    }
}

4.Angular检测机制

Ok,我们现在已经知道Angular怎么监听异步事件了,那么当监测到异步事件后是怎么判断是否需要更新视图呢?其实比较简单,Angular通过脏检查来判断是否需要更新视图。脏检查其实就是存储所有变量的值,每当可能有变量发生变化需要检查时,就将所有变量的旧值跟新值进行比较,不相等就说明检测到变化,需要更新对应视图。当然,实际情况肯定不是这么简单,Angular会通过自己的算法来对数据进行检查,对算法感兴趣的可以参考这篇文章-Angular的脏检查算法。
Angular 应用是一个响应系统,首次检测时会检查所有的组件,其他的变化检测则总是从根组件到子组件这样一个从上到下的顺序开始执行,它是一棵线性的有向树,任何数据都是从顶部往底部流动,即单向数据流。怎么证明呢?看这个例子

app.component.html

<h1>变更检测</h1>
<input type="button" value="改名" (click)="changeName()">
<app-rank-parent [parentName]="grandpaName"></app-rank-parent>
<app-refer [justRefer]="refertitle"></app-refer>
app.component.ts

import {Component, Input, OnChanges, OnInit} from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnChanges {
  @Input() grandpaName: string = '赵四';
  refertitle:string = '仅仅是个参考组件';


  constructor() {
  }

  ngOnInit() {
    console.log('开始变更检测啦!');
  }

  ngOnChanges(changes) {
    console.dir(changes);
  }

  changeName() {
    this.grandpaName = '尼古拉斯·赵四';
    this.refertitle = '你看Onpush策略下面Input属性还有效,气不气人';
  }
}
rank-parent.component.html

<app-rank-children [childName]="parentName"></app-rank-children>
rank-parent.component.ts

import {Component, Input, OnChanges, OnInit} from '@angular/core';

@Component({
  selector: 'app-rank-parent',
  templateUrl: './rank-parent.component.html',
  styleUrls: ['./rank-parent.component.css']
})
export class RankParentComponent implements OnInit, OnChanges {
  @Input()parentName;

  constructor() {}

  ngOnChanges(changes) {
    console.dir(changes);
  }

  ngOnInit() {}
}
rank-children.component.html

<p>
  儿子姓名是:{{childName}}三代
</p>
rank-children.component.ts

import {Component, Input, OnChanges, OnInit} from '@angular/core';

@Component({
  selector: 'app-rank-children',
  templateUrl: './rank-children.component.html',
  styleUrls: ['./rank-children.component.css']
})
export class RankChildrenComponent implements OnInit,OnChanges {
  @Input() childName;

  constructor() { }

  ngOnChanges(changes) {
    console.dir(changes);
  }

  ngOnInit() {
  }

}

运行以后我们会得到如下结果,可以看到首次检测时检查了所有组件,包括ReferComponent,检测从上到下逐个检测。点击改名按钮后再次检测时则只检测有变化的那一侧组件(RankParentComponent,RankChildrenComponent)。其中我们可以观察到,虽然在AppComponent中输入属性也发生了变化并且也更新了视图,但是ngOnChanges钩子却没有检测到变化,注意这是一个坑。

E5D5779F-A086-4FAE-8F69-6B25BF3E02A2.png

那么什么是单向数据流呢?其实简单理解就是angular检测到数据变化到更新完视图的过程中数据是不应该被改变的,如果我们在这期间更改了数据,Angular便会抛出一个错误,举个例子,我们在RankChildrenComponent的ngAfterViewChecked钩子函数中更改childName的值,在控制台会看到如下错误。

import {AfterViewChecked, Component, Input, OnChanges, OnInit} from '@angular/core';

@Component({
  selector: 'app-rank-children',
  templateUrl: './rank-children.component.html',
  styleUrls: ['./rank-children.component.css']
})
export class RankChildrenComponent implements OnInit,OnChanges,AfterViewChecked {
  @Input() childName;

  constructor() { }

  ngOnChanges(changes) {
    console.dir(changes);
  }

  ngOnInit() {
  }

  ngAfterViewChecked(){
    this.childName  = "我要试着改变一下"
  }

}
817D8548-4393-4920-B44B-B7D1B5BAC320.png

如果必须要更改这个属性的值,能不能做呢?答案是可以的。结合刚次提到的单向数据流,如果我们把这次数据变更放到下一轮Angular变更检测中,就能解决这个问题了。怎么做呢?刻意异步一下就行了。是不是很神奇?

ngAfterViewChecked(){
  setTimeout(()=>{
    this.childName  = "我要试着改变一下"
  },0)
}

至于angular为什么要采用单向数据流,其实也很好理解,最主要的就是防止数据模型和视图不统一,同时也可以提高渲染的性能。

4.如何优化检测性能

讲了这么多,所以到底有什么用呢?其实在 Angular 中,每一个组件都都它自己的检测器(detector),用于负责检查其自身模板上绑定的变量。所以每一个组件都可以独立地决定是否进行脏检查。默认情况下,变化检测系统将会走遍整棵树(defalut策略),但我们可以使用OnPush变化检测策略,利用 ChangeDetectorRef实例提供的方法,来实现局部的变化检测,最终提高系统的整体性能。
来,举个例子。在ReferComponent中,我们设个定时器2秒以后更新一个非输入属性的值,在默认策略时,可以发现2秒以后视图中的值发生了改变,但是当我们把策略改为Onpush时,除了在AppComponent点击按钮改变输入属性justRefer外,其他属性改变不会引起视图更新,ReferComponent组件的检测也被略过。我们可以这么总结:OnPush 策略下,若输入属性没有发生变化,组件的变化检测将会被跳过。

refer.component.ts
import {ChangeDetectionStrategy, Component, Input, OnChanges, OnInit} from '@angular/core';

@Component({
  selector: 'app-refer',
  templateUrl: './refer.component.html',
  styleUrls: ['./refer.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ReferComponent implements OnInit, OnChanges {
  @Input() justRefer;
  notInput: any = {
    tip: ''
  };
  originData: any = {
    tip: '这不是输入属性'
  };

  constructor() {
  }

  ngOnInit() {
    setTimeout(() => {
      this.notInput = this.originData;
    }, 2000);

  }

  ngOnChanges(changes) {
    console.dir(changes);
  }

}

可是我就是要更改非输入属性怎么办呢?别急,Angular早就为你想好了。在Angular中,有这么一个class:ChangeDetectorRef ,它是组件的变化检测器的引用,我们可以在组件中的通过依赖注入的方式来获取该对象,来手动控制组件的变化检测行为。它提供了以下方法供我们调用

class ChangeDetectorRef {
  markForCheck(): void
  detach(): void
  detectChanges(): void
  checkNoChanges(): void
  reattach(): void
}
  • markForCheck() - 在组件的 metadata 中如果设置了 changeDetection: ChangeDetectionStrategy.OnPush 条件,那么变化检测不会再次执行,除非手动调用该方法。
  • detach() - 从变化检测树中分离变化检测器,该组件的变化检测器将不再执行变化检测,除非手动调用 reattach() 方法。
  • reattach() - 重新添加已分离的变化检测器,使得该组件及其子组件都能执行变化检测
  • detectChanges() - 从该组件到各个子组件执行一次变化检测

现在我们来试试解决刚才那个问题,我们对ReferComponent做如下改造。

constructor(private changeRef:ChangeDetectorRef) {
}

ngOnInit() {
  setTimeout(() => {
    this.notInput = this.originData;
    this.changeRef.markForCheck();
  }, 2000);

}

ok,现在看到在Onpush策略下手动修改非输入属性的值,视图也可以及时更新了。其他的几个方法也都大同小异,感兴趣的可以逐个试试。

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

推荐阅读更多精彩内容