Angular Change Detection 的工作原理分析

举报
Jerry Wang 发表于 2022/10/02 09:37:22 2022/10/02
【摘要】 Angular 变化检测机制比 AngularJs 中的等效机制更透明且更易于推理。但是在某些情况下(例如在进行性能优化时),我们确实需要知道幕后发生了什么。因此,让我们通过以下主题深入了解变更检测:如何实施变更检测?Angular 变化检测器是什么样子的,我能看到吗?默认的变更检测机制是如何工作的打开/关闭更改检测,并手动触发它避免变更检测循环:生产与开发模式什么是OnPush变化检测模式...

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 不同的是,变更检测机制是可定制的。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。