動態

詳情 返回 返回

Angular Change Detection 的學習筆記 - 動態 詳情

Angular 變化檢測機制比 AngularJs 中的等效機制更透明且更易於推理。但是在某些情況下(例如在進行性能優化時),我們確實需要知道幕後發生了什麼。因此,讓我們通過以下主題深入瞭解變更檢測:

  • 如何實施變更檢測?
  • Angular 變化檢測器是什麼樣子的,我能看到嗎?
  • 默認的變更檢測機制是如何工作的
  • 打開/關閉更改檢測,並手動觸發它
  • 避免變更檢測循環:生產與開發模式
  • 什麼是OnPush變化檢測模式實際上呢?
  • 使用 Immutable.js 簡化 Angular 應用程序的構建

如何實施變更檢測?
Angular 可以檢測到組件數據何時發生變化,然後自動重新渲染視圖以反映該變化。但是,在像單擊按鈕這樣的低級事件之後,它怎麼能做到這一點,這可能發生在頁面的任何地方?

要理解這是如何工作的,我們需要首先意識到在 Javascript 中整個運行時(runtime)在設計上是可重載的。如果我們願意,我們可以重載 String 或者 Number 這些原生函數。

Overriding browser default mechanisms

Angular 應用在啓動時,會 patch 幾個低級瀏覽器 API,例如 addEventListener,它是用於註冊所有瀏覽器事件(包括單擊處理程序)的瀏覽器函數。

Angular 將其替換addEventListener 的另一個新版本:

// this is the new version of addEventListener
function addEventListener(eventName, callback) {
     // call the real addEventListener
     callRealAddEventListener(eventName, function() {
        // first call the original callback
        callback(...);     
        // and then run Angular-specific functionality
        var changed = angular.runChangeDetection();
         if (changed) {
             angular.reRenderUIPart();
         }
     });
}

新版本 addEventListener為任何事件處理程序添加了更多功能:不僅調用了註冊的回調,而且 Angular 有機會運行更改檢測和更新 UI。

這種低級運行時的 patch 動作是如何工作的?

瀏覽器 API 的這種低級 patch 是由一個名為Zone.js 的Angular 附帶的庫完成的。瞭解什麼是區域很重要。

區域只不過是在多個 Javascript VM 執行輪次中倖存下來的執行上下文。這是一種通用機制,我們可以使用它向瀏覽器添加額外的功能。Angular 在內部使用 Zones 來觸發更改檢測,但另一個可能的用途是進行應用程序分析,或跟蹤跨多個 VM 輪次運行的長堆棧跟蹤。

支持瀏覽器異步 API

Patch 了以下常用瀏覽器機制以支持更改檢測:

  • 所有瀏覽器事件(點擊、鼠標懸停、按鍵等)
  • setTimeout() 和 setInterval()
  • Ajax HTTP 請求

事實上,Zone.js patch 了許多其他瀏覽器 API 以透明地觸發 Angular 更改檢測,例如 Websockets。

這種機制的一個限制是,如果由於某種原因,Zone.js 不支持異步瀏覽器 API,則不會觸發更改檢測。

這解釋瞭如何觸發更改檢測,但是一旦觸發它實際上是如何工作的?

The change detection tree

每個 Angular 組件都有一個關聯的變更檢測器,它是在應用程序啓動時創建的。

下面是一個例子:

@Component({
    selector: 'todo-item',
    template: `<span class="todo noselect" 
       (click)="onToggle()">{{todo.owner.firstname}} - {{todo.description}}
       - completed: {{todo.completed}}</span>`
})
export class TodoItem {
    @Input()
    todo:Todo;

    @Output()
    toggle = new EventEmitter<Object>();

    onToggle() {
        this.toggle.emit(this.todo);
    }
}

該組件將接收一個 Todo 對象作為輸入,並在 todo 狀態切換時發出一個事件。 為了使示例更有趣,Todo 類包含一個嵌套對象:

export class Todo {
    constructor(public id: number, 
        public description: string, 
        public completed: boolean, 
        public owner: Owner) {
    }
}

在 Todo 類的代碼裏設置一個斷點:

當上圖第11行代碼觸發時,我們在調試器裏觀察上下文:

How does the default change detection mechanism work?

這個方法一開始可能看起來很奇怪,所有的變量名字都很奇怪。 但是通過深入研究它,我們注意到它做了一些非常簡單的事情:對於模板中使用的每個表達式,它將表達式中使用的屬性的當前值與該屬性的先前值進行比較。

如果前後的屬性值不同,就會設置 isChanged為true,原理就是這樣。實際上,它是通過使用一種名為 looseNotIdentical() 的方法來比較值
,這實際上只是與 NaN 情況下的特殊邏輯的 === 比較。

源代碼如下:

And what about the nested object owner?

我們可以在 change detector 代碼中看到,owner 嵌套對象的屬性也在進行變更檢查。

但只比較 firstname 屬性,而不是 lastname 屬性。

這是因為在組件模板中沒有使用 lastname 這個屬性。同樣,Todo 的頂級 id 屬性也未進行比較。

默認情況下,Angular Change Detection 通過檢查模板表達式(template expression)的值是否已更改來工作。 這是為所有組件完成的。

並且,Angular 不做深度對象比較來檢測變化,它只考慮模板使用的屬性。

The OnPush change detection mode

如果我們的 Todo 列表變得非常大,我們可以將 TodoList 組件配置為僅在 Todo 列表更改時更新自身。 這可以通過將組件更改檢測策略更新為 OnPush 來完成:

@Component({
    selector: 'todo-list',
    changeDetection: ChangeDetectionStrategy.OnPush,
    template: ...
})
export class TodoList {
    ...
}

現在讓我們嚮應用程序添加幾個按鈕:一個通過直接改變它來切換列表的第一項,另一個將 Todo 添加到整個列表。 代碼如下所示:

@Component({
    selector: 'app',
    template: `<div>
                    <todo-list [todos]="todos"></todo-list>
               </div>
               <button (click)="toggleFirst()">Toggle First Item</button>
               <button (click)="addTodo()">Add Todo to List</button>`
})
export class App {
    todos:Array = initialData;

    constructor() {
    }

    toggleFirst() {
        this.todos[0].completed = ! this.todos[0].completed;
    }

    addTodo() {
        let newTodos = this.todos.slice(0);
        newTodos.push( new Todo(1, "TODO 4", 
            false, new Owner("John", "Doe")));
        this.todos = newTodos;
    }
}

測試結果:

  • 第一個按鈕“切換第一項”不起作用! 這是因為 toggleFirst() 方法直接改變了列表的一個元素。
    TodoList 無法檢測到這一點,因為它的輸入引用 todos 沒有改變
  • 第二個按鈕能夠工作。 請注意,方法 addTodo() 創建了待辦事項列表的副本,然後在副本中添加了一個項目,最後將 todos 成員變量替換為複製的列表。 這會觸發更改檢測,因為組件檢測到其輸入中的引用更改:它收到了一個新列表。

當使用 OnPush 檢測器時,當 OnPush 組件的任何輸入屬性發生變化、觸發事件或 Observable 觸發事件時,框架將對該組件進行變更檢測。

Angular 變更檢測的重要特性之一是,與 AngularJs 不同,它強制執行單向數據流:當我們的控制器類上的數據更新時,變更檢測會運行並更新視圖。

但是,視圖的更新本身不會觸發進一步的更改。假設這些被視圖更新觸發的進一步更新,又會回過頭來觸發對視圖的進一步更新,這就是 AngularJs 中所謂的摘要循環(digest cycle)。

總結

Angular 變化檢測是一個內置的框架功能,可確保組件的數據與其 HTML 模板視圖之間的自動同步。

變更檢測的工作原理是檢測常見的瀏覽器事件,如鼠標點擊、HTTP 請求和其他類型的事件,並決定是否需要更新每個組件的視圖。

有兩種類型的變化檢測:

  • 默認更改檢測:Angular 通過比較事件發生前後的所有模板表達式值來決定是否需要更新視圖。
  • OnPush 更改檢測:這通過檢測是否已通過組件輸入或使用異步管道訂閲的 Observable 將某些新數據顯式推送到組件中來工作。

Angular 默認更改檢測機制實際上與 AngularJs 非常相似:它比較瀏覽器事件前後模板表達式的值,以查看是否發生了變化。它對所有組件都這樣做。但也有一些重要的區別:

  • 一方面,沒有變化檢測循環,也沒有在 AngularJs 中命名的摘要循環。這允許僅通過查看其模板和控制器來推理每個組件。
  • 另一個區別是,由於構建變化檢測器的方式,檢測組件變化的機制要快得多。

最後,與 AngularJs 不同的是,變更檢測機制是可定製的。

更多Jerry的原創文章,盡在:"汪子熙":

user avatar wenroudemangguo 頭像 fanudekaixinguo 頭像 zhoumo_62382eba4b454 頭像 ethanprocess 頭像 xiangjian_659d190d45a7b 頭像 huopodelianpen 頭像
點贊 6 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.