這是一篇故事,就如同技術,咱們所追求的不是一個結局,而是那些深受啓發與共鳴的過程,那是咱們成長的經驗與生產力的積澱!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團隊去解決吧。