尋找真兇Echarts or Angular

尋找真兇Echarts or Angular

這是一篇故事,就如同技術,咱們所追求的不是一個結局,而是那些深受啓發與共鳴的過程,那是咱們成長的經驗與生產力的積澱!javascript

故事開始於「瘋了」的ionic3應用

頁面打開,什麼也沒作5s裏angular的代碼彷佛一直在跑!
clipboard.pngjava

打開chrome性能調試工具,recorded 5秒,密密麻麻的調用棧,慘不忍睹!
clipboard.pngangularjs

Qustion1:難道真兇是angular髒檢查,發生了循環髒檢查??要弄清這個問題前,咱們先來介紹angular髒檢查這個大人物。chrome

Rope1:angular2及以上版本髒檢查方式

新一代的angular一改angularjs(ng1)中受人唾棄的髒檢查策略。segmentfault

數據流的改變

angularjs的策略::是again and again直到穩定。也就是說在異步事件觸發髒檢查後,髒檢查發生過程當中某一個scope值改變後,會再次觸發一次髒檢查直到scope上數據穩定不變。這樣一個過程很難找到一次髒檢查是哪一次、哪個對象發生改變致使的dom更新。
angular的策略::從組件樹頂至下,各組件依次作本身的髒檢查。以下圖,左邊是model右邊是dom樹也是組件樹,每一次model數據的改變,觸發一次髒檢查,每次檢查從跟節點開始單向向下,在此次檢查時間片斷中不會容許對model作修改,model數據處於穩定狀態。
clipboard.png瀏覽器

誰告訴angular作髒檢查的改變

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

Rope2: angular髒檢查策略

clipboard.png

先來看看現場,上面的圖中圈出了一段代碼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不會有任何更新。以下圖: 異步

clipboard.png

而當click方法觸發時,該組件會進行髒檢查並更新dom。

若是使用了OnPush策略,又想讓resize中的修改能能更新dom怎麼辦?代碼以下

constructor(private ref: ChangeDetectorRef) {}
  resize() {
    this.option.title = 'Hi'
    this.ref.markForCheck();
  }

依然是從上到下,angular會找到包含該組件的路徑的全部component進行逐一髒檢查(即便頂層組件設置了onPush策略)以下圖:
clipboard.png

answer2:很顯然組件樹龐大不會引發髒檢查多,由於咱們已經加了onPush策略,input也未改變,該組件及該組件向下的組件都不該該發生髒檢查
雖然加了onPush策略但頁面上依然有不少不應運行的代碼一直在執行,下圖爲頁面穩定靜止狀態下記錄5s內的瀏覽器執行狀況,左圖爲未加onPush策略的記錄,右圖爲已加onPush策略的記錄,能夠看見已加onPush策略的依然有script,render,Painting在執行。

clipboard.png
clipboard.png

咱們再來看一下調用棧,以下圖:

clipboard.png

從圖中咱們發現了一個調用棧NgZone的代碼執行過,還記得Rope1裏提到NgZone嗎?發起髒檢查的通知者,它代理了原生事件,任何一個原生異步事件的觸發都會致使NgZone的運行。那麼必定是有原生事件在一直Loop執行!

【注:細心的人可能還發現圖裏有一些同窗會發現有angular.core的代碼在執行,不是在answer2中已經說了不會髒檢查了嗎?確實不會在作髒檢查,rope2中也說明過髒檢查策略的原理,別忘了再髒檢查前還會check組件input引用來決定是否該組件作髒檢查呢】

Qustion3:誰在調戲NgZone?
咱們再繼續看下性能分析裏的調用棧,只要該函數進入過"犯罪現場"咱們都能找到它的足跡。Look this!咱們找到了一個animation.js執行的step函數。

clipboard.png

look this!果真有一個requestAnimationFrame定時器()原生事件一直在執行,且從未銷燬!
clipboard.png

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如圖:

clipboard.png

故事的結局

雖然優化的結果不是最完美的,從圖上能夠看到頁面穩定靜止狀態下仍是有script(echarts的bad code)在執行。如何去解決echarts的loop requestAnimationFrame問題,後續提issue留給echarts團隊去解決吧。

相關文章
相關標籤/搜索