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: `
    <h1>{{firstname}} {{lastname}}</h1>
    <button (click)="changeName()">Change name</button>
  `
})
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下去,总会有那么一刻你会看到下面这张图:


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来对模板进行数据更新。


image.png

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

image.png

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

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

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

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

image.png

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

推荐阅读更多精彩内容