在應用程式的開發過程中,state 代表需要顯示在應用程式上的資料。當 state 發生變化時,往往需要一個機制來偵測變化的 state 並隨之更新對應的介面。這個機制就叫做 Change Detection 機制。 【相關教學推薦:《angular教學》】
在 WEB 開發中,更新應用程式介面其實就是對 DOM 樹進行修改。由於 DOM 操作是昂貴的,所以一個效率低的 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 將替換
為與此等效的新版本:<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:js;toolbar:false;">// 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();
}
});
}</pre><div class="contentsignin">登入後複製</div></div>
新的
setTimeout()
和#事實上,Zone.js 修補了許多其他瀏覽器API,以透明地觸發Angular 更改檢測,例如Websockets。
每個 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 狀態切換時發出事件。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:js;toolbar:false;">export class Todo {
constructor(public id: number,
public description: string,
public completed: boolean,
public owner: Owner) {
}
}</pre><div class="contentsignin">登入後複製</div></div>
我們可以看到 Todo 有一個屬性owner
,它本身就是一個有兩個屬性的物件:firstname
和
我們實際上可以在運行時看到變化檢測器的樣子!要查看它,只需在 Todo 類別中添加一些程式碼以在存取某個屬性時觸發斷點。
當斷點命中時,我們可以遍歷堆疊追蹤並查看變化檢測:
這個方法一開始可能看起來很奇怪,所有變數都奇怪命名。但是透過深入研究,我們注意到它正在做一些非常簡單的事情:對於模板中使用的每個表達式,它會將表達式中使用的屬性的當前值與該屬性的先前值進行比較。 如果前後的屬性值不同,就會設定
isChanged
為true,就這樣!差不多,它是透過使用一個名為
那麼巢狀物件owner我們可以在更改偵測器程式碼中看到
owner
巢狀物件的屬性也正在檢查差異。但只比較 firstname
屬性,而不是 lastname
屬性。這是因為元件template
中沒有使用
!同樣,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 成员变量替换为复制的列表。这会触发更改检测,因为组件检测到其输入中的参考更改:它收到了一个新列表!OnPush
只是通过引用比较输入吗?情况并非如此。当使用 OnPush 检测器时,框架将在 OnPush 组件的任何输入属性更改、触发事件或 Observable 触发事件时检查
尽管允许更好的性能,但OnPush
如果与可变对象一起使用,则使用会带来很高的复杂性成本。它可能会引入难以推理和重现的错误。但是有一种方法可以使使用OnPush
可行。
如果我们只使用不可变对象和不可变列表来构建我们的应用程序,则可以OnPush
透明地在任何地方使用,而不会遇到更改检测错误的风险。这是因为对于不可变对象,修改数据的唯一方法是创建一个新的不可变对象并替换之前的对象。使用不可变对象,我们可以保证:
OnPush
更改检测实现不可变的一个不错的选择是使用Immutable.js库。该库为构建应用程序提供了不可变原语,例如不可变对象(映射)和不可变列表。
Angular 更改检测的重要属性之一是,与 AngularJs 不同,它强制执行单向数据流:当我们的控制器类上的数据更新时,更改检测运行并更新视图。
一种方法是如果我们使用生命周期回调。例如,在TodoList组件中,我们可以触发对另一个组件的回调来更改其中一个绑定:
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默认更改检测机制实际上与 AngularJs 非常相似:它比较浏览器事件之前和之后模板表达式的值,以查看是否有更改。它对所有组件都这样做。但也有一些重要的区别:
一方面,没有变化检测循环,也没有 AngularJs 中命名的摘要循环。这允许仅通过查看其模板和控制器来推理每个组件。
另一个区别是,由于变化检测器的构建方式,检测组件变化的机制要快得多。
最后,与 AngularJs 不同的是,变化检测机制是可定制的。
更多编程相关知识,请访问:编程教学!!
以上是淺析Angular中的Change Detection機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!