淺析Angular中的Change Detection機制

2022-12-15 22:00:15

前端(vue)入門到精通課程,老師線上輔導:聯絡老師
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API偵錯工具:

什麼是 Change Detection ?

在應用的開發過程中,state 代表需要顯示在應用上的資料。當 state 發生變化時,往往需要一種機制來檢測變化的 state 並隨之更新對應的介面。這個機制就叫做 Change Detection 機制。【相關教學推薦:《》】

在 WEB 開發中,更新應用介面其實就是對 DOM 樹進行修改。由於 DOM 操作是昂貴的,所以一個效率低下的 Change Detection 會讓應用的效能變得很差。因此,框架在實現 Change Detection 機制上的高效與否,很大程度上決定了其效能的好壞。

Change Detection 是如何實現的

Angular 可以檢測元件資料何時更改,然後自動重新渲染檢視以反映該更改。但是在像點選按鈕這樣的低階事件之後,它怎麼能做到這一點呢?

通過 Zone , Angular 能夠實現自動的觸發 Change Detection 機制

Zone 是什麼呢?簡而言之,Zone 是一個執行上下文(execution context),可以理解為一個執行環境。與常見的瀏覽器執行環境不同,在這個環節中執行的所有非同步任務都被稱為 Task ,Zone 為這些 Task 提供了一堆的勾點(hook),使得開發者可以很輕鬆的「監控」環境中所有的非同步任務。

題外話:由於 Angular 極力的推崇使用可觀察物件(Observable),如果完全的基於 Observable 來開發應用,可以代替 Zone 來實現追蹤呼叫棧的功能,且效能還比使用 Zone 會稍好一些。

  // Angular 在 v5.0.0-beta.8 起可以通過設定不使用 Zone 
  import { platformBrowser } from '@angular/platform-browser';
  platformBrowser().bootstrapModuleFactory(AppModuleNgFactory, { ngZone: 'noop' });
登入後複製

覆蓋瀏覽器預設機制

Angular 在啟動時會重寫瀏覽器 low-level 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。

支援瀏覽器非同步 API

修補了以下常用瀏覽器機制以支援更改檢測:

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

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

這種機制的一個限制是,如果由於某種原因 Zone.js 不支援的非同步瀏覽器 API,則不會觸發更改檢測。例如,IndexedDB 回撥就是這種情況。

預設的變更檢測機制是如何工作的?

每個 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 狀態被切換時發出一個事件。

export class Todo {
    constructor(public id: number, 
        public description: string, 
        public completed: boolean, 
        public owner: Owner) {
    }
}
登入後複製

我們可以看到 Todo 有一個屬性owner,它本身就是一個具有兩個屬性的物件:firstnamelastname

變更檢測器是什麼樣的?

我們實際上可以在執行時看到變化檢測器的樣子!要檢視它,只需在 Todo 類中新增一些程式碼以在存取某個屬性時觸發斷點。

當斷點命中時,我們可以遍歷堆疊跟蹤並檢視變化檢測:

image.png

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

如果前後的屬性值不同,就會設定isChanged 為true,就這樣!差不多,它是通過使用一個名為looseNotIdentical() 的方法來比較值。

那麼巢狀物件owner呢?

我們可以在更改檢測器程式碼中看到 owner 巢狀物件的屬性也正在檢查差異。但只比較 firstname 屬性,而不是 lastname 屬性。這是因為元件template中沒有使用lastname!同樣,Todo 的頂級 id 屬性也沒有出於相同的原因進行比較。

有了這個,我們可以有把握地說:

預設情況下,Angular Change Detection 通過檢查模板表示式的值是否已更改來工作。

我們還可以得出結論:

預設情況下,Angular 不做深度物件比較來檢測變化,它只考慮模板使用的屬性

為什麼預設情況下更改檢測會這樣工作?

Angular 的主要目標之一是更加透明和易於使用,因此框架使用者不必費盡心思偵錯框架並瞭解內部機制即可有效地使用它。

如果 Angular 預設更改檢測機制基於元件輸入的參考比較而不是預設機制,那會是什麼情況?即使是像 TODO 應用程式這樣簡單的東西也很難構建:開發人員必須非常小心地建立一個新的 Todo,而不是簡單地更新屬性。

OnPush 變化檢測策略

如果你覺得預設模式影響了效能,我們也可以自定義 Angular 更改檢測。將元件更改檢測策略更新為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()建立了 todo 列表的副本,然後將專案新增到副本中,最後將 todos 成員變數替換為複製的列表。這會觸發更改檢測,因為元件檢測到其輸入中的參考更改:它收到了一個新列表!
  • 在第二個按鈕中,直接改變 todos 列表是行不通的!我們真的需要一個新的清單。

OnPush只是通過參照比較輸入嗎?

情況並非如此。當使用 OnPush 檢測器時,框架將在 OnPush 元件的任何輸入屬性更改、觸發事件或 Observable 觸發事件時檢查

儘管允許更好的效能,但OnPush如果與可變物件一起使用,則使用會帶來很高的複雜性成本。它可能會引入難以推理和重現的錯誤。但是有一種方法可以使使用OnPush可行。

使用 Immutable.js 簡化 Angular 應用程式的構建

如果我們只使用不可變物件和不可變列表來構建我們的應用程式,則可以OnPush透明地在任何地方使用,而不會遇到更改檢測錯誤的風險。這是因為對於不可變物件,修改資料的唯一方法是建立一個新的不可變物件並替換之前的物件。使用不可變物件,我們可以保證:

  • 新的不可變物件將始終觸發OnPush更改檢測
  • 我們不會因為忘記建立物件的新副本而意外建立錯誤,因為修改資料的唯一方法是建立新物件

實現不可變的一個不錯的選擇是使用庫。該庫為構建應用程式提供了不可變原語,例如不可變物件(對映)和不可變列表。

避免變更檢測迴圈:生產與開發模式

Angular 更改檢測的重要屬性之一是,與 AngularJs 不同,它強制執行單向資料流:當我們的控制器類上的資料更新時,更改檢測執行並更新檢視。

如何在 Angular 中觸發變更檢測迴圈?

一種方法是如果我們使用生命週期回撥。例如,在元件中,我們可以觸發對另一個元件的回撥來更改其中一個繫結:

ngAfterViewChecked() {
    if (this.callback && this.clicked) {
        console.log("changing status ...");
        this.callback(Math.random());
    }
}
登入後複製

控制檯中將顯示一條錯誤訊息:

EXCEPTION: Expression '{{message}} in App@3:20' has changed after it was checked
登入後複製

僅當我們在開發模式下執行 Angular 時才會丟擲此錯誤訊息。如果我們啟用生產模式會發生什麼? 在生產模式下,錯誤不會被丟擲,問題也不會被發現。

在開發階段始終使用開發模式會更好,因為這樣可以避免問題。這種保證是以 Angular 總是執行兩次變更檢測為代價的,第二次檢測這種情況。在生產模式下,變更檢測只執行一次。

開啟/關閉變化檢測,並手動觸發它

在某些特殊情況下,我們確實想要關閉更改檢測。想象一下這樣一種情況,大量資料通過 websocket 從後端到達。我們可能只想每 5 秒更新一次 UI 的某個部分。為此,我們首先將更改檢測器注入到元件中:

constructor(private ref: ChangeDetectorRef) {
    ref.detach();
    setInterval(() => {
      this.ref.detectChanges();
    }, 5000);
  }
登入後複製

正如我們所看到的,我們只是分離了變化檢測器,這有效地關閉了變化檢測。然後我們只需每 5 秒通過呼叫手動觸發它detectChanges()

現在讓我們快速總結一下我們需要了解的關於 Angular 變更檢測的所有內容:它是什麼,它是如何工作的以及可用的主要變更檢測型別是什麼。

概括

Angular 更改檢測是一個內建的框架功能,可確保元件資料與其 HTML 模板檢視之間的自動同步。

更改檢測的工作原理是檢測常見的瀏覽器事件,如滑鼠點選、HTTP 請求和其他型別的事件,並確定每個元件的檢視是否需要更新。

變更檢測有兩種型別:

  • 預設更改檢測:Angular 通過比較事件發生前後的所有模板表示式值來決定是否需要更新檢視,用於元件樹的所有元件
  • OnPush 更改檢測:這通過檢測是否已通過元件輸入或使用非同步管道訂閱的 Observable 將某些新資料顯式推播到元件中來工作

Angular預設更改檢測機制實際上與 AngularJs 非常相似:它比較瀏覽器事件之前和之後模板表示式的值,以檢視是否有更改。它對所有元件都這樣做。但也有一些重要的區別:

一方面,沒有變化檢測迴圈,也沒有 AngularJs 中命名的摘要回圈。這允許僅通過檢視其模板和控制器來推理每個元件。

另一個區別是,由於變化檢測器的構建方式,檢測元件變化的機制要快得多。

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

更多程式設計相關知識,請存取:!!

以上就是淺析Angular中的Change Detection機制的詳細內容,更多請關注TW511.COM其它相關文章!