在了解 AngularJS 的 Digest Cycle 机制后,对 Angular 的 Change Detection 机制也产生了好奇。看了许多相关的文章和视频,有了些本身的理解,在此作个总结、记录,并分享一些本身的想法。博文原地址。javascript
在应用的开发过程当中,state 表明须要显示在应用上的数据。当 state 发生变化时,每每须要一种机制来检测变化的 state 并随之更新对应的界面。这个机制就叫作 Change Detection 机制。html
在 WEB 开发中,更新应用界面其实就是对 DOM 树进行修改。因为 DOM 操做是昂贵的,因此一个效率低下的 Change Detection 会让应用的性能变得不好。所以,框架在实现 Change Detection 机制上的高效与否,很大程度上决定了其性能的好坏。前端
在谈 Angular 的 Change Detection 机制以前,咱们先来看一下他的「前辈」(AngularJS)的 Digest Cycle 老是被人诟病的缘由。java
Digest Cycle 是 AngularJS Change Detection 机制的核心。AngularJS 会为每个绑定在 HTML 上的变量建立一个 watcher,这种形式建立的 watcher 我称其为 bind_watcher 。另外,开发者也能够经过 watch( ) 方法手工的为指定的变量建立一个 watcher,我称其为 manul_watcher 。同一个 state 会有多个 bind_watcher 但只会有一个 manul_watcher 。这两种 watcher 都会被存放在 $scope.$$watchers 这个数组中。至于脏查询(Dirty Checking)的实质其实就是遍历某个 scope 及其全部 child_scope 下的 $$watchers,并校验每个 watcher 是否脏(dirty)了。若脏了,调用对应的监听器(listener),bind_watcher 中的 listener 会修改对应的 DOM 节点,而 manul_watcher 的 listener 则是开发者自定义的一个函数。因为 listener 中可能会修改已经被检测过的 state (即这个 state 对应的 watcher 已经被检测过了),为了尽量的保证在 Digest Cycle 后全部的 watcher 都是出于「干净」的状态,AngularJS 就不得不进行屡次(缺省上限为10次)的 Dirty Checking 。能够结合下图进行理解。node
上述即是 Digest Cycle 的基本原理。一个 AngularJS 应用颇有可能产生成百上千的 watcher ,这种须要进行屡次 Dirty Checking 的机制极其低效,因此 AngularJS 应用的性能老是被人诟病。git
题外话:网上常说 AngularJS 的脏查询(Dirty Checking)怎么怎么很差,其实 Dirty Checking 自己并无什么问题,问题在于 AngularJS 的 Change Detection 是在其特有的 watcher 机制的基础上来实现的,再加上其混乱的数据流,才不得不进行效率极低的 Digest Cycle 。这也是为何 AngularJS 团队明知 Digest Cycle 是如此的低效,却又没法进行完全重写的缘由,由于不是简单的重写 Digest Cycle 自己就够了,还须要从新考虑 watcher 机制、数据流等一些方面的问题,这也是 Angular 不基于 AngularJS 来重写缘由之一。github
在了解完 AngularJS 的 Digest Cycle 以后,咱们根据如下几个问题,逐步的了解 Angular 的 Change Detection 机制。web
在使用 AngularJS 开发应用时,若是咱们须要使用定时器,有两种方法:编程
使用浏览器原生的 setTimeout( ) 方法,但须要注意:若是在定时器中有修改 state ,那么须要调用 $scope.$digest( ) 方法来确保修改后的 state 可以反应在 UI 界面上。bootstrap
使用 AngularJS 内置的 $timeout 服务,使用它的好处是不须要手工的调用 $scope.$digest( ) 方法来刷新页面。
其实这两种方法本质都是同样的,就是:AngularJS 须要手动调用 $scope.$digest( ) 方法来触发框架的 Change Detection 。
而对于 Angular ,我先抛出结论:经过 Zone , Angular 可以实现自动的触发 Change Detection 机制。接下来咱们就来看看 Angular 是如何实现的。
在全部的 web 应用中,有如下三种场景须要触发 Change Detection:
它们共同的特色就是:都是异步。而 Zone 是什么呢?简而言之,Zone 是一个执行上下文(execution context),能够理解为一个执行环境。与常见的浏览器执行环境不一样,在这个环节中执行的全部异步任务都被称为 Task ,Zone 为这些 Task 提供了一堆的钩子(hook),使得开发者能够很轻松的「监控」环境中全部的异步任务。
在 Angular 中,框架会生成一个 zone ,大部分的代码都在这个 zone 中执行,如此一来,Angualr 就能够监控全部的异步任务。举个例子:假如组件 A 中有一个 setTimeout( ) 方法,一旦其回调函数被执行,就会触发 zone 的 onInvoke 钩子,而后在这个钩子中去触发 Change Detection 机制。这也就实现了「自动」触发 Change Detection 的效果。
注:ZoneJS 做为一个独立的库,在其余方面还有许多的用途,我后续会写一篇单独讲 ZoneJS 的问题。
题外话:因为 Angular 极力的推崇使用可观察对象(Observable),若是彻底的基于 Observable 来开发应用,能够代替 Zone 来实现追踪调用栈的功能,且性能还比使用 Zone 会稍好一些。Angular 在 v5.0.0-beta.8 起能够经过配置不使用 Zone ,配置以下:
import { platformBrowser } from '@angular/platform-browser'
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory, { ngZone: 'noop' })
复制代码
组件化是如今前端发展的主要趋势之一,每个页面都是由一个个组件组成的,这些组件构成一颗组件树。在常规状况下,Angular 依旧使用的 Dirty Checking,也就是被你们说厌了的脏查询。Angular 会从根组件开始,逐一检测每个组件。与 AngularJS 复杂的数据流不一样,Angular Change Detection 是基于组件树的单向数据流来实现的。组件树+单项数据流使得 Angular 在检测每个组件时不须要考虑当前组件会修改父组件的数据而不得不像 AngularJS 那样进行屡次的 Dirty Checking 。
结论1:Angular 的组件树+单向数据流解决了 AngularJS 中须要屡次 Dirty Checking 的问题,即只需一次 Dirty Checking 就可以完成 Change Detection 。
值的一提的是,在开发模式下,Angular 会有意的进行两次 Dirty Chacking(因此在开发的应用的时候感受应用的性能不是很好)。其目的是提示开发者:你开发的代码违背了单向数据流的策略。举个例子:当检测某个组件时,会触发组件的 DoCheck 钩子,若是开发者在这个钩子中经过某种方式(好比 Observable)修改了父组件的状态,这种作法是不符合单向数据流的(由于父组件已经被检测过),虽然在开发模式下对应的 UI 界面也会正常的被修改,可是 Angualr 会打印出错误,提示开发者这种行为是不被容许的。具体能够参考示例代码:angular-unidirection-error-demo,能够打开控制看看对应的错误。
Victor Savkin(Angular 的核心开发成员之一)在 2015 的演讲当中有提到以下的一个公式:
CHANGE DETECTION TIME = C * N
C - time to check a bind
N - number of binding
翻译成中文就:完成一次 Change Detection 所须要花费的时间 =(约等于) 检测一个组件变化所需的时间 * 绑定 state 的个数。
为了提升 Change Detection 的速度,咱们只需下降 C 或 N 便可。接下来咱们来看看 Angular 是如何下降 C 的,即如何减小检测单个变化所需的时间的(后面我还会谈及在某些状况下,为了更高的性能,能够经过下降 N 来实现性能的提供。)。
无论是 AngularJS 仍是 Angular 的 Change Detection ,它们都须要经过检测器(Detector)来检测差别。在 AngularJS 中,其检测器的伪码相似以下:
// Detector1
while (scope) {
var getter = watcher.get;
var oldValue = op.last;
var newValue = getter(scope);
if (oldValue !== newValue) {
op.last = newValue;
var fn = watcher.listener;
fn(oldValue, newValue);
}
scope = scope.next;
}
复制代码
这段代码很好理解,没什么问题。可是,Angular 以为这样的代码显然还不够快,VM(虚拟机) 并不喜欢这样「动态」的代码。你可能会问了:VM 会喜欢什么样的代码?我告诉你,VM 喜欢「单纯」的代码,好比:
// Detector2
// 组件A的html模板:<h1>{{data.title}}</h1>
// 组件A对应的 detector
class A_ChangeDetector {
detectChanges() {
var data = obj.data;
var title = data.title;
if (title !== this.last) {
this.last = title;
// 更新对应的 DOM 节点
this.node.innerHTML = title;
}
}
}
复制代码
对比上述的两个 Detector 会发现:
Detector1 是一个通用的一个检测器,而 Detector2 是一个针对组件A的检测器。Detector2 的代码更加的简单,或者说是更加的「单纯」。
这也就解释了为何 VM 会喜欢 Detector2 的实现了。因此能够得出的结论是:
Detector2 运行的会比 Detector1 更快。
谈到这里,你可能会有一个和我最初同样,产生一个问题:我认可 Detector2 比 Detector1 会稍微快一点,可是快的程度应该是微乎其微,就算采用 Detector2 的检测方法,优化的程度应该也不怎么明显吧?
这就涉及到一个「数量级」的问题了。诺是单纯的检测某一个 state ,Detector2 比 Detector1 两个检测器的检测速度肉眼根本没法判断它们之间的快慢。 可是随着须要被检测的 state 的数量不断的增长,Detector2 细微的优化就会被不断的放大,采用 Detector2 的应用的性能也就比采用 Detector1 的性能高出不少。
不错,Angular 就是使用相似 Detector2 的检测器。Angular 在 rumtime(若是是 AOP 编译,则是在 Compile-time 的时候) 的时候会为每个组件建立一个对应的 Change Detector ,因为这些 Detector 就和上述的 Detector2 同样是:VM friendly code(VM 更加喜欢的代码) ,因此在某个数量级上检测的性能忽悠很大的提高,后面我会写一个 Demo 来直观的感觉各个框架性能的差别。
至此,大体讲完了 Angular Change Detection 常规的实现机制。接下来谈谈:Angular 还能够更快?
在一些场景中,咱们会异常的关注应用的性能,好比移动端开发,好比大面积的画面渲染。接下来,咱们来谈谈:Angular 其实还能够更快。
仍是前面讲的那个公式:CHANGE DETECTION TIME = C * N 。若是须要提升 Change Detection 的效率,咱们还能够减小 N ,即经过减小须要被检测的 state 的数量来提升检测效率。
在某些场景下,做为开发者,咱们实际上是可以知道哪些组件是能够不用检测的。Angular 就提供了这种能力:告诉Angular 那些组件在什么状况下是不须要检测的,其中就涉及到了一个概念:Immnutable Object 。
咱们日常开发当中所使用的对象基本都是 Mutable Object ,好处是可以很大程度的节省内存(由于都是在同一个对象上进行修改)。但也有如下两个缺点:
export function touchAndPrint(touchFn) {
var data = { key: 'value' };
touchFn(data);
console.log(data.key); // ?
}
复制代码
如上,若是 touchFn( ) 这个方法是别的同事写的,我会根本知不道最后输出 data.key 是什么,由于 data 是一个 Mutable Object ,在 touchFn( ) 中可能会被修改。能够看出 Mutable Object 有一种不可预测的性质,会对开发过程产生没必要要的一些困扰,这也是为何函数式编程变的愈来愈受欢迎的缘由。
因为是 Mutable Object 能够在同一个对象上修改某个值,因此经过 === 比较符并不能判断两个对象中的每个值是否相等。因此咱们一般会对对象进行深度遍历,逐一比较每个值。若是只是一些简单的对象那也还好,但若是频繁的比较复杂的对象,可能就会影响应用的性能了。
回到咱们要讲的 Immutable Object ,也就是「没法被修改的对象」。Immutable Object 的优缺点和 Mutable Object 正好相反,缺点是若是每一次对对象的某个属性赋值都产生一个新的对象,这显然会占据大量的内存。可是显示有些第三方库如:Immutable.js,它经过一些特殊的机制解决了这个问题。如今 Immutable Object 在许多框架中都有被使用到,特别是 React 。
常规的 Change Detection 都是从跟组件开始进行脏查询,以下图:
做为开发者,咱们有时是可以知道并不须要检测全部的组件,咱们只想检测部分组件,以下图:
是的,Angular 提供了这种能力,其原理就是基于 Immutable Object 来实现的。
Angular 能够经过如下代码来告诉对应的检测器:若是组件的 state 是一个对象,那么你不须要检测对象里每个值得变化状况(由于这个对象是 Immutable Object 类型),你只需简单的判断它和旧值是否绝对相等,即判断:newObjec === oldObject 既可。若是没有检测到组件的变化,那就没有必要检测其子组件树的变化了,由于开发者说了:若是我没变,个人子组件是不会变的。
@component({
selector: 'xxx',
templateUrl: 'xxx',
changeDetection: ChangeDetectionStrategy.OnPush
})
复制代码
但愿上述的描述你可以大体的理解设置 ChangeDetectionStrategy.OnPush 的做用。接着讲讲什么时候可使用 ChangeDetectionStrategy.OnPush 。
每次 Change Detection 之因此是从根组件开始从上至下进行检测,是由于任何一个组件均可以经过某个服务来间接的修改任意组件的 state(注意这和单向数据流并不矛盾,Angular 中的单向数据流指的是某一个具体的 state 只能从上往下流动,父组件能把 state 传给子组件,而子组件只能够将某个事件透传给父组件,其并不能够传递具体的 state 给父组件。)。
小结一下:Anuglar 提供了一种方式来优化 Change Detection 的性能表现,这种方式是基于 Immutable Object 并经过检测某个组件是否 Change 来决定检测器是否继续检测其全部子树的方式来尽量的减小须要被检测的组件数量,以达到提升检测的效率的目的。
在优化 Angular Change Detection 的过程当中,经常会使用到 ChangeDetectionRef 中提供的方法,下面我经过图文并茂的方式逐一解释其中的每个方法。
该方法能够理解为:在执行这个方法后的第一次 Change Detection 中,忽视当前组件或是父组件中 ChangeDetectionStrategy.OnPush 对所在 Change Detection 的影响。
该方法会从当前组件开始触发一次 Change Detection 。
该方法会从当前组件开始触发一次 Change Detection ,若是有检测某个组件 Change ,抛出异常并中止检测。
开发者能够经过这两个方法手工的将所在组件与其子组件分离或是从新链接。
为了可以更加直观的感觉对 Angular Change Detection 优化后的表现,我写了一个项目,比较了 AngularJS Anuglar、React、Vue 四种框架在某一特定场景的性能,其中 Anuglar 有两个版本,分别是 normal 版本和 faster 版本,另外两个框架都是 normal 版本。而这个项目描述的场景是:加载一个日历,在渲染日历的过程须要不断的向服务端(用 setTimeout 取代了 http 请求)获取数据。项目地址:ChangeDetectionCompare
提早声明:这里的性能比较不是为了说明 Angular 比另外两个框架都话,关于这点,我后面会稍微谈谈本身我的的想法,这次比较只是单纯的感觉对 Angular Change Detection 优化后的表现。
如下是各个版本在我电脑上完成渲染所花费的时间(都是在生产环境)。
这里的数据都是执行三次,取速度最快的一次,数据仅供参考。
至于具体的优化原理,结合上述对 Angular Change Detection 的解释,再看看项目的代码,相信你可以看懂。