Angular变化检测

前言

在AngularJs1.x中,经常会遇到不能被angular捕捉的一些模型变化,导致模板得不到更新。比如在原生setTimeout里对$scope模型进行更新,就会导致angular的捕获不到。从AngularJs转到Angular2+之后,竟然也会遇到这样的问题,在项目中经常会发现模型,模板不更新。这个时候的解决方案往往有以下几种:

  • 使用Angular的ChangeDetectRef下的detectChange()方法强制触发更新检测
  • 将更新模型的操作包在NgZone.run()里,通过zone来触发更新。

虽然也是解决了问题,但这个一直困扰着我的Angular变化检测。我今天一定要把它搞明白! 这篇文章主要翻译自下面这个文章,另外加了点自己的见解,有不对的地方,还望指出:

https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html#who-notifies-angular

When?

废话不多说,先上代码:

@Component({
  template: `
    

{{firstname}} {{lastname}}

` }) class MyApp { firstname:string = 'Pascal'; lastname:string = 'Precht'; changeName() { this.firstname = 'Brad'; this.lastname = 'Green'; } }

这个component的功能很简单,初始的时候显示姓名,点击button时,改变姓名。想必这些对于我们都是小菜一碟了。在这个例子里,属性会在click事件发生时进行更新,那么显然我们这个时候就需要更新模板了哇。
再来一个例子:

@Component()
class ContactsApp implements OnInit{

  contacts:Contact[] = [];

  constructor(private http: Http) {}

  ngOnInit() {
    this.http.get('/contacts')
      .map(res => res.json())
      .subscribe(contacts => this.contacts = contacts);
  }
}

在这个例子里,会去调用一个API请求,在其回调里来更新contacts模型。同样这个时候也是我们应该更新模板的时刻了。
废话了这么多, 重点来了。看下面这句话就行:

通常会在以下三种情况下触发一个应用状态的改变,从而需要更新模板:
(1) Events - click, submit, …
(2) XHR - API请求相关
(3) Timers - setTimeout(), setInterval()

聪明的人一定会发现他们的共同点,就是他们都是异步操作,所以针对when?
这个问题就可以稍作总结:就是一般在有异步操作的情况下, 我们的就需要来告诉Angular去更新对应的模板。

Who?

所以问题又来了,当这些异步操作发生时,得去告诉Angular更新我们的模板,那这个得罪人的事谁来说呢?正如我最开始所说的,当模板不更新的时候,会用ChangeDetectRef或NgZone去强制更细模板,这其中的原理,这里就不细讲,他涉及到Angular另外一个核心知识,Zone。想了解更多,可以看下面两篇文章。

https://blog.thoughtram.io/angular/2016/01/22/understanding-zones.html
https://blog.thoughtram.io/angular/2016/02/01/zones-in-angular-2.html

但是我们不可能每次都手动去做这些事,那就太不智能了,那样的话,我们为什么还需要用框架干嘛,又退回到JQuery时代了。
所以Angular框架那么多代码不是白写的,他里面有这样一个东东,叫ApplicationRef,这个东东会监听NgZone的onTurnDone事件。只要这个事件被触发,就会调用tick()方法执行变化监测。不信的话你可以针对你自己写的异步事件代码一直F11下去,总会有那么一刻你会看到下面这张图:


Angular变化检测_第1张图片
image.png

这里还是插播一下NgZone的onTurnDone事件吧:

追根溯源还得从浏览器的循环机制说起。对于异步事件,浏览器有这么个东西叫事件队列,这些队列会暂时性存放这些异步事件队列,等到他们该执行的时候。放到执行栈中来执行。关于事件循环机制那些事,真不是我几句话能说的清楚的,推荐一篇还算简单易懂的文章:https://www.cnblogs.com/pzy-123/articles/7245473.html。回到这个onTurnDone,和这个onTurnDone相关的还有另外onTurnStart,onEventDone。他们都是NgZone提供的一些自定义事件,Angular将这些事件都封装成Observable事件流,因此我们自然而然可以对这些事件流进行订阅。所以下面根据事件循环机制来理解,事件循环队列有很多的事件,而zone turn这一过程即是将事件循环队列里的事件从队列里取出的过程。所以:

  • onTurnStart() - 在我们的Angular框架将事件从循环队列里取出之前,就触发了这个事件了。
  • onTurnDone() - 将队列里的事件放入执行栈执行,执行完之后,就触发了这个事件。
  • onEventDone() - 最后一个onTurnDone()执行完,事件循环结束之前,即队列里没有事件可处理之前触发。
    值得注意的是,大家会发现,在上面我的代码截图里订阅的事件似乎不叫onTurnDone().
    这是因为自Angular2 beta版之后,这几个事件就已经换名字了, 是的, 你还没有开始,他就已经变了,这就是前端技术,好伤有木有,不过我个人觉得换了名字的的事件更形象,更好理解了:
    但是他这里取名叫MicrotaskEmpty, 一开始以为只会执行微任务,但是实际上自己写代码试了下setTimeout,也是执行的,所以不要被误导。
    NgZone.onTurnStart => NgZone.onUnstable
    NgZone.onTurnDone => NgZone.onMicrotaskEmpty
    NgZone.onEventDone => NgZone.onStable
    下文还是会先用onTurnDone ,毕竟是翻译过来的么。

以上逻辑的代码片段可以总结为以下模式:

// 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());
  }
}

源码远比这个复杂,想了解源码的可以看@Angular/core这个包里的application_ref.ts文件。
同样的,我们可以总结如下下到底是谁来告诉Angular来更新的: 我们可以把这个Zone理解为一个模拟了浏览器事件循环机制的一个库,并且做了一些在事件发生的前后都做了一些hook,每一个异步事件对应一个task的话,当我们把这些task从事件队列里取出,放入执行栈里执行,并执行结束的时候,就会告诉Angular的Application_ref这个类“我的数据拿到了,该执行的回调处理也执行完了,你快点看看有没有模板上需要更新的数据,有的话就麻利儿更新”。tick()这个方法就来完成相应的更新操作了。

How?

OK, 讲了这么多,所以请告诉我到底是怎样更新的?不要着急,来来来,回忆一下我们做项目的时候,是怎样写的?对于单页应用,是不是bootstrap的永远就那么一个熟悉的AppComponent, 然后衍生出儿子,孙子,孙子的孙子,...。所以对于一个Angular Application来说,他是一个由component组成的树型结构。
Angular这个框架为每个Component都分配了一个Change Detector,这样的一一对应关系使得生成的变化检测树也是一个树型结构。也正是这个change detector来对模板进行数据更新。


Angular变化检测_第2张图片
image.png

这个变化检测树同时也是可以被当做一个数据从顶部向下流到底部的有向图,这里会涉及到Angular2+的另外一个特性,即Angular的单向数据流特性,它不同于Angular1.x的环形数据流特性,这种单向数据流使得Angular在数据传播时更简单,更纯粹。这种数据传播的方式也正是由于Angular的变化检测树是自上向下进行变化检测的。

Angular变化检测_第3张图片
image.png

还有个有趣的事情,就是经过一轮检测树检测过后,变化检测会趋于稳定,这个时候若想再做出什么变化,angular就会报错。不信大家可以尝试下在ngAfterViewChecked这个钩子函数里试图改变下已经渲染好的模板的模型,看看他报什么错。

以上就是从三个层面分析了变化检测相关。OK, 不知道你有没有明白如果还是不明白,其实我觉得有必要好好读一下ngZone的代码,这也是我未来想去做的一件事情。

关于变化检测这一块,还有一个很重要的东西,就是性能问题,如何优化变化检测性能问题。但我相信一篇文章不宜过长,否则会失去耐心,所以请听下回分解。

另外附上GitHub demo实例: https://github.com/thoughtram/angular2-change-detection-demos
关于这个实例,不知道为什么要在ngAfterViewChecked里进行样式变更,这样会给人一种从bottom往root变更检测的误解。个人感觉样式变更写在DoChecked里比较好。
因此下面这种层级关系的component,默认情况下,不论异步事件发生在哪个子组件上,都会触发整个变化检测树从顶到下的一个变更检测。

Angular变化检测_第4张图片
image.png

Angular变化检测_第5张图片
image.png

你可能感兴趣的:(Angular变化检测)