
年前在公司举行了一场盛况空前(误)的前端分享会,我在会上大谈特谈了关于 Angular 的变化检测机制,就在即将迎来完美的收官之时,总是会有淘气鬼提出各种奇奇怪怪的问题,让人不胜其烦(大误)。由于当时无言以对、支支吾吾,便只好会后一番仔细研究后,发出文章以正视听。望日后能弓调马服再大谈特谈(逃
文中提及的 Angular 源码均基于 Angular 官方仓库 tag 4.4.6
onPush 也会触发 View 更新?
Question
淘气鬼的第一个问题是关于 onPush 策略下 View 的更新问题。
首先是一段网上举例再多不过的关于使用 Observable + onPush 来减少组件变化检测的代码:
1 | ({ |
毫无疑问地,了解过 ChangeDetectionStrategy.OnPush
的同学都明白,此时 sub-a 组件的 local
值虽然被改变了,但由于没有调用 markForCheck
方法,且所有 @Input
属性没有发生改变,Angular 会跳过该组件(及其子组件)的所有变化检测,因此页面并不会更新。去掉这个 markForCheck
方法的注释,页面就能够得到更新了。具体可以去查阅其他关于 OnPush
策略的文章。
好了,这时候淘气鬼说,那如果我们不是改变一个 @Input
的变量,而是一个内部变量呢?View 会更新吗?即:
1 | ({ |
这里直接把父子组件间的变量传递去掉了,方便理解。我一开始是认为View 不会更新,原因是既然输入属性没有发生改变,那这个 SubA
组件的变化检测就应该被 skip 掉。然而:
他变了。
简直击碎了我的世界观。
更诡异的情况
在我阅读众多资料对 OnPush
策略与 Angular 变化检测的理解中,点击按钮后所发生的应该是:
click event => ngZone 捕获,开始一次从根的变化检测 => 到 OnPush 策略的组件,跳过 => 页面不会更新
是哪里出了问题呢?抱着疑问在 Google 的帮助下四海为家(误),期间也咨询了不少人,但也得不到满意的答复,直到在官网的 API 上看到这样一个例子:
1 | ({ |
这个例子跟淘气鬼的例子很像,区别只在于官网使用了 setInterval
来触发 ViewModel 的更新,而淘气鬼就搞了个按钮。
但是,这个 setInterval
居然是不会更新 View,而点击按钮却是会更新 View,纳尼?这两个对 zone 来说不是都是异步吗?都是触发一轮变化检测吗?
把淘气鬼的例子改了下,发现的确如此,调用同一个函数 change
,每一秒的定时任务 View 不会更新,点击按钮界面就能更新!WHY?如此看来那应该是 ngZone
对这两种异步事件的处理有区别。
1 | ({ |
调用绑定事件会自动 markForCheck
在 StackOvewflow 上搜索一番,果然还是皇天不负有心人啊
Angular OnPush Component, when trigger event in view angular force markForCheck automatically?
原来原因是,对于 DOM 中绑定的事件,会在内部自动把 markForCheck
帮你调用了。来我们看看源码:
1 | // packages/core/src/view/provider.ts - L134 |
这里只需要看到绑定事件的处理函数是 eventHandlerClosure
,而这个处理函数则是调用了 dispatchEvent
。
1 | // packages/core/src/view/util.ts - L128 |
而 dispatchEvent
则是调用了 markParentViewsForCheck
。没错就是这个通知了 Angular 触发 View 更新,我们来看看 ChangeDetectorRef.markForCheck
的源码。
1 | // packages/core/src/view/refs.ts - L248 |
没错,markForCheck
的内部就是通过 markParentViewsForCheck
实现的。
总结下来,其实我的理解是没错的,使用了 OnPush
策略,无论如何改变变量,只要没有 markForCheck
,View 的确是不会更新的。
OnPush 会更新 View 的情况
那篇 StackOverflow 有个介绍 OnPush
策略下什么情况会更新 View 的总结,顺带也搬运一下。
Change Detection issue — Why is this changing when it’s the same object reference with On Push
@Input
属性改变
这是最通常的理解了。内部通过将 ViewState
置为 ChecksEnabled
使组件重新加入变化检测。
1 | // packages/core/src/view/provider.ts - L424 |
组件内触发绑定事件
也就是上文分析的情况。
手动调用 markForCheck
这个也没有问题,手动把该组件重新设置为可 Check。
Async pipe
这个我也真没想到,async pipe 内部也自动调用了 markForCheck
,不过考虑到它连销毁都自带了,这也可以理解。
1 | // packages/common/src/pipes/async_pipe.ts - L139 |
ViewCheck 的执行顺序
Question
这是淘气鬼的第二个问题,在展示组件生命周期钩子调用顺序的时候,有如下的组件树(丑)结构:
请问他的 ngAfterViewChecked
钩子的执行顺序是啥呢?
我一开始以为会是 BDCA,没错就是一个树的后序遍历。但现实却是 DBCA!这是什么鬼!
到底是谁 Check 谁?
这个其实怪我一时没有明白 Change Detection 的步骤,这里强烈推荐我认为全网写得最好的关于 Angular 变化检测的文章:
Everything you need to know about change detection in Angular
里面总结了对于每个组件一轮变化检测要执行的步骤,重点关注 10) 和 12) 步:
runs change detection for a child view (repeats the steps in this list)
call
AfterViewInit
andAfterViewChecked
lifecycle hooks on child component instance (AfterViewInit
is called only during first check)
整个步骤看下来,我们发现了这样两件事。
第一,子组件的递归变化检测是在调用 ViewCheck
钩子前进行的。
第二,比如,当组件 A 执行完一轮变化检测后,A.ngAfterViewChecked
并没有被调用。仔细查看每一个步骤,对组件 A 的变化检测中,确实没有调用 A 的 ViewCheck
钩子这样的步骤。那这个钩子是在什么时候调用的呢?
回到上面提到的 12) 步,对于每个组件的一轮变化检测,需要去调用子组件的 ViewCheck
钩子。因此,比如,A 组件的 ViewCheck
钩子是谁调用的呢?就是 A 组件的父组件调用的,而不是 A 自身,是 A 的父组件去确认 A 已经 Check 了。
理解了这两个以后,上面那个组件树的 ViewCheck
调用顺序就非常清晰了。
详细描述
针对上图的组件树,只 focus 递归变化检测和 ViewCheck 下的具体调用情况如下:
- A Begin Change detection
- A 组件有两个子组件 B/C
- B Begin Change detection
- B 调用所有子组件的 ViewCheck 钩子(无输出)
- B End Change detection
- C Begin Change detection
- C 组件有子组件 D
- D Begin Change detection
- D 调用所有子组件的 ViewCheck 钩子(无输出)
- D End Change detection
- C 调用所有子组件的 ViewCheck 钩子(输出 D)
- C End Change detection
- A 调用所有子组件的 ViewCheck 钩子(输出 B C)
- A End Change detection
这样下来,DBCA 的输出就非常显而易见了。
无奖问答
看看你掌握了吗?下面这个组件树的 ViewCheck
钩子顺序是啥呢?
答案看后文的图片。
ContentCheck
值得注意的是,ContentCheck
的顺序跟 ViewCheck
十分类似,也是父组件的变化检测中调用子组件的钩子,只不过这是在递归之前进行的调用。
上文的两个组件树的 ContentCheck
顺序分别为:ABCD 和 ABDCEF,看看能不能明白?
总结
总的来说,淘气鬼的问题基本上都解决了,算是可喜可贺,普天同庆了。Angular 果真是博大精深,看源码都要看晕了,不过也多亏了淘气鬼,让我对 Angular 变化检测的理解又上了一个台阶。另外也可以看出像 Angular 这样的大框架要是没有 TypeScript 来支持,真的是鬼才看得懂它的代码咯。
- 本文标题:Angular 变化检测中的细节
- 创建时间:2018-02-28 14:04:34
- 本文链接:details-in-angular-detect-change/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!