1. 程式人生 > >Angular4 元件變化檢測總結 (Change Detection)

Angular4 元件變化檢測總結 (Change Detection)

看了很多有關的文章,也認真觀看了Angular2 團隊放在youtube的相關演講視訊。(你懂的)

我也想用自己的理解總結一下Angular2/4元件變化的原理。內容分這麼幾個小節:

  1. 什麼操作會引發元件繫結屬性變化
  2. 元件繫結屬性變化如何通知Angular2
  3. Angular2如何進行髒檢查(與Angular1簡單對比)
  4. 小結:為什麼Angular2比Angular1效能好很多

什麼操作會引發元件繫結屬性變化

其實不光是Angular,對於所有的MVVM框架,引發繫結屬性變化的方式無非下面四種:
Events: click, submit etc..
XHR

: 從後臺伺服器獲取資料更新檢視
Timers: setTimeout(), setInterval()
Promise: ES6 新出

以上四種方式的一個共同點就是:它們全部都是非同步命令。這就引出了下面的問題:

元件繫結屬性變化如何通知Angular2——關鍵字 Zone.js

問題:為什麼非同步指令難以跟蹤捕捉:

function doSomething() {
  console.log('Async task');
}

// start timer
start = timer();
foo();
setTimeout(doSomething, 2000
); bar(); baz(); // stop timer time = timer() - start;

我們用試圖得到程式碼的執行時間,但是很顯然包裹在setTimeout中的doSomething的執行時間無法計算,foo, bar, baz都在執行棧中而doSomething由於setTimeout的原因被放入了事件佇列。

Solution: Zone.js的引入

Zone最著名的一個標籤就是猴子補丁(Monkey-patched Hooks)
那麼什麼是猴子補丁呢,用一句話概括就是將所有javascript的原生方法特別是關於非同步的重新實現一遍,既然是自己重建那當然可以夾雜一些私貨在裡面了(比如監控非同步函式的執行)。

這些重寫的方法包括但不僅限於以下,大家可以感受一下:
window.addEventListener = Zone.addEventListener,
window.removeEventListener = Zone.removeEventListener,
window.setTimeout = Zone.setTimeout,
window.setInterval = Zone.setInterval

除了重寫了一些非同步方法,Zone還定義了自己的event,當Angular的繫結屬性改變,將會觸發這些event,其中最關鍵的event 叫 onTurnDone。而這個event 的 handler 就是做 Change Detection. 直接上原始碼可能更清晰一點:

ObservableWrapper.subscribe(this.zone.onTurnDone, () => {
  this.zone.run(() => {
    this.tick();
  });
});

tick() {
  // perform change detection
  this.changeDetectorRefs.forEach((detector) => {
    detector.detectChanges();
  });
}

Angular2如何進行髒檢查(與Angular1簡單對比)

下面是一個關於監測資料改變次數的公式(來自Angular2 團隊的演講視訊)

這裡寫圖片描述

C - 檢查一個繫結所花的的時間
N - 繫結的數目

由上圖我們可以看到,提高效率的方法無非兩種:減少 C 或者 減少 N.

減少 C

與Angular1相比,Angular2 在原始碼層面做了很多優化,號稱幾毫秒可以做數萬次監測,畢竟是換車了,所以 C 大大的降低了。所以在 C 上,angular2 完勝!

減少 N。這是我想要著重來說的。

這裡有兩個重點:

  1. 有向樹結構。
  2. Immutable Object

Angular2 的有向樹結構:

Angular1:
這裡寫圖片描述

Angular2:
這裡寫圖片描述

從這張angular1 和 angular2 的對比圖可以看出:在 binding 數目一樣的情況下,環結構可能對同一個 binding 檢查多遍,而有向樹至多檢查一遍。從這個角度來說,Angular2 的 binding 數目比 Angular1 要少很多 (特別是大型專案)

Angular2 的 Immutable Object:

Mutable(可變) VS Immutable(不可變)

Mutable:

Javascript 預設所有的物件都是可變的,Angular1 並沒有引入 Immutable,所以 Angular1 中所有的物件也都是可變的。那麼什麼是可變呢:

var employee = {
    salary: 1000,
    age: 30
};
var anotherEmployee = employee;
anotherEmployee.salary = 10000000;
console.log(employee === anotherEmployee) // true

很明顯,employee 和 anotherEmployee 的引用相同,改變 employee 中的屬性當然也就相當於改變了 anotherEmployee。這就是物件可變。

Immutable:

var employee = {
    salary: 1000,
    age: 30
};
var anotherEmployee = Object.assign({}, employee, {salary: 10000000});;
console.log(employee === anotherEmployee); // false

在修改 employee 物件中的 salary 時,我們並沒有直接修改原有物件,而是用 Object.assign 方法建立一個新的物件anotherEmployee。這就是物件不可變( Immutable Object )。

可以推匯出一個小結論:在 javascript 中,Object 型別都是 mutable,其他諸如 number,string,boolean 都是 immutable

我們再回頭想想 angular1 和 angular2 中的繫結語法:

<app [employee] = 'employee'> </app>

對於 mutable 的情況,當繫結的值是一個物件,那麼當該物件中的某個屬性發生變化時,對於 Angular (不管1還是2) 來說,該物件是沒有變化的,但是業務要求這個變化必須要檢測到,怎麼辦呢,沒有辦法,框架只好檢查每一個 binding 的每一個屬性來和前值做對比。

Change Detect

既然 Angular1 和 Angular2 中的物件都是 mutable,體現不出 Angular2 的優越性, 那還扯半天所為何來? 呵呵不好意思,我少加了一個形容詞,在 Angular2 中物件預設是 mutable。預設的意思就是我們可以改變讓它不預設。

這裡寫圖片描述

我們可以在定義Component的時候將 Change Detect 的檢查策略改為 OnPush

@Component({
  template: `
    <h2>{{employee.salary}}</h2>
    <span>{{employee.age}}</span>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
class EmployeeCmp {
  @Input() employee;
}

當繫結物件變成 immutable 的時候,改變繫結物件的屬性就意味著物件本身的改變,那麼 Angular2 就很容易檢測從而實行“定點清除”,如下圖所示:
OnPush

這樣當然又有效的減少了 N 次數。

以上就是 Angular2 的元件繫結變化總結,希望對花時間來閱讀它的人有所幫助。通過自己寫一寫部落格總結一下,對這塊掌握的也更牢固了。

最後,今天是 2017 年的最後一個工作日,希望自己在接下來的一年中能繼續前行,心想事成:)