这是一篇故事,就如同技术,咱们所追求的不是一个结局,而是那些深受启发与共鸣的过程,那是咱们成长的经验与生产力的积淀!javascript
页面打开,什么也没作5s里angular的代码彷佛一直在跑!java
打开chrome性能调试工具,recorded 5秒,密密麻麻的调用栈,惨不忍睹!angularjs
Qustion1:难道真凶是angular脏检查,发生了循环脏检查??要弄清这个问题前,咱们先来介绍angular脏检查这个大人物。chrome
新一代的angular一改angularjs(ng1)中受人唾弃的脏检查策略。segmentfault
angularjs的策略::是again and again直到稳定。也就是说在异步事件触发脏检查后,脏检查发生过程当中某一个scope值改变后,会再次触发一次脏检查直到scope上数据稳定不变。这样一个过程很难找到一次脏检查是哪一次、哪个对象发生改变致使的dom更新。
angular的策略::从组件树顶至下,各组件依次作本身的脏检查。以下图,左边是model右边是dom树也是组件树,每一次model数据的改变,触发一次脏检查,每次检查从跟节点开始单向向下,在此次检查时间片断中不会容许对model作修改,model数据处于稳定状态。浏览器
angularjs的方式: 注入ng事件来通知脏检查,例如,你不能在js原生的setTimeout里改变model值,必须注入ng事件$setTimeout。
angularjs的方式: zone.js (它也是个big man,想了解它能够看个人一篇NgZone.js文章https://segmentfault.com/a/11...)。什么都不用作,原生随意写,天然有家伙帮你通知angular去作脏检查。
answer1:从以上线索能够判定,不是angular发生了循环脏检查 angular2
Qustion2:是否是从组件树顶向下逐一组件进行脏检查,会不会是组件树太庞大log了太多checked,执行了太屡次单个组件脏检查?echarts
先来看看现场,上面的图中圈出了一段代码changeDetection: ChangeDetectionStrategy.OnPush,它是作什么的呢?它能够改变脏检查的策略。上面提到一颗组件树中某一个节点某个event触发了脏检查,整棵组件树每个节点都会跟着作脏检查对吧?对的,默认的策略是这样的。但angular能够更聪明点,使用OnPush策略。这个策略会让该个组件在input对象引用指针没发生变化时跳过该节点及该节点子节点脏检查(注意:是对象引用指针的变化)。
example 1:dom
@Component({ selector: 'echart', template: `<div class="charts" #root></div>`, changeDetection: ChangeDetectionStrategy.OnPush }) export class ChartComponent { @Input('option') option: any; constructor() { } ngOnInit(): void { window.addEventListener('resize', this.resize, true); } click():void{ this.option= { title:'hi' } } resize() { this.option.title = 'Hi' } }
这个例子中,当页面窗口发生变化是resize中修改title,,dom不会有任何更新。以下图: 异步
而当click方法触发时,该组件会进行脏检查并更新dom。
若是使用了OnPush策略,又想让resize中的修改能能更新dom怎么办?代码以下
constructor(private ref: ChangeDetectorRef) {} resize() { this.option.title = 'Hi' this.ref.markForCheck(); }
依然是从上到下,angular会找到包含该组件的路径的全部component进行逐一脏检查(即便顶层组件设置了onPush策略)以下图:
answer2:很显然组件树庞大不会引发脏检查多,由于咱们已经加了onPush策略,input也未改变,该组件及该组件向下的组件都不该该发生脏检查
虽然加了onPush策略但页面上依然有不少不应运行的代码一直在执行,下图为页面稳定静止状态下记录5s内的浏览器执行状况,左图为未加onPush策略的记录,右图为已加onPush策略的记录,能够看见已加onPush策略的依然有script,render,Painting在执行。
咱们再来看一下调用栈,以下图:
从图中咱们发现了一个调用栈NgZone的代码执行过,还记得Rope1里提到NgZone吗?发起脏检查的通知者,它代理了原生事件,任何一个原生异步事件的触发都会致使NgZone的运行。那么必定是有原生事件在一直Loop执行!
【注:细心的人可能还发现图里有一些同窗会发现有angular.core的代码在执行,不是在answer2中已经说了不会脏检查了吗?确实不会在作脏检查,rope2中也说明过脏检查策略的原理,别忘了再脏检查前还会check组件input引用来决定是否该组件作脏检查呢】
Qustion3:谁在调戏NgZone?
咱们再继续看下性能分析里的调用栈,只要该函数进入过"犯罪现场"咱们都能找到它的足迹。Look this!咱们找到了一个animation.js执行的step函数。
look this!果真有一个requestAnimationFrame定时器()原生事件一直在执行,且从未销毁!
answer3:原来流氓是echarts的animation.js或者说是echarts核心组件zrender在动画结束后没调用animation中的stop方法,总之真凶是echarts!(若是你正在使用echarts,能够打开调试工具,能够看到那段代码一直在loop执行)
凶手找到了,受害者还须要安抚解决,如何解决?弃用echarts?你要知道有一种流氓叫让你讨厌又让你干不掉,不得不认可echarts的绘制效率在移动端仍是不错的,还有地图,用其它chart plugin谁来给你画某某市地图...
此时不得再也不捧一把Angular,虽然咱们管不了echarts,但NgZone是一个很开放的家伙。给咱们不少自由操做的空间,就像下面的sample,使用runOutsideAngular将包裹的函数内部执行的代码都跳过zone.js的包装。那个echarts的requestAnimationFrame不再会骚扰我们的NgZone了。
export class EChartsComponent implements OnInit, OnDestroy { @Input() chartid: string; @Input('option') option: any; private chart: any; @ViewChild('root') private root; constructor(private ngZone: NgZone) { } resizeListener = () => this.resize(); ngOnInit(): void { this.ngZone.runOutsideAngular(() => { this.chart = echarts.init(this.root.nativeElement); this.chart.setOption(this.option, true); window.addEventListener('resize', this.resizeListener, true); }) } }
优化后5s内perfermance如图:
虽然优化的结果不是最完美的,从图上能够看到页面稳定静止状态下仍是有script(echarts的bad code)在执行。如何去解决echarts的loop requestAnimationFrame问题,后续提issue留给echarts团队去解决吧。