游乐游手机版
首页/前端开发/文章详情

Angular变更检测机制实现原理详解

时间:2026-06-16 07:05
Angular通过Zone js自动触发变更检测,默认机制是比较模板表达式的值来更新视图。支持OnPush策略,仅当输入引用或事件触发时检查,可配合不可变对象优化性能。开发模式下会检测单向数据流违规,生产模式只运行一次检测。

什么是 Change Detection?深度解析 Angular 变更检测机制

在应用开发过程中,state(状态)直接决定了界面上呈现的数据内容。当 state 发生改变时,必须有一套机制能够察觉这一变化,并同步更新对应的界面。这套机制,就是我们常说的 Change Detection(变更检测)。

放在 Web 开发的语境下,更新界面本质上就是修改 DOM 树。由于 DOM 操作的开销非常巨大,如果你的变更检测机制效率低下,应用的性能就会急剧下降。因此,一个框架的变更检测实现是否高效,往往直接决定了它的性能天花板究竟能有多高。

Change Detection 的实现原理

Angular 能够精准感知组件数据何时发生变化,并自动重新渲染视图。但问题来了:在类似点击按钮这种极为底层的事件发生后,它是如何实现“自动”触发的呢?

答案就是 Zone。通过 Zone.js,Angular 能够自动触发变更检测流程。

Zone 到底是什么?通俗地讲,它是一段“执行上下文”,或者说执行环境。与我们常见的浏览器执行环境不同,在这个环境里执行的所有异步任务都被称为 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 在启动时,会重写许多浏览器底层 API。比如addEventListener,这个函数用于注册所有浏览器事件,包括点击处理。Angular 会将其替换为功能类似的全新版本:

// 这是 addEventListener 的新版本
function addEventListener(eventName, callback) { 
    // 先调用真正的 addEventListener
    callRealAddEventListener(eventName, function() { 
        // 首先执行原始的回调
        callback(...);
        // 然后运行 Angular 特有的逻辑
        var changed = angular.runChangeDetection();
        if (changed) {
            angular.reRenderUIPart();
        }
    });
}

这样一来,新的addEventListener就在任何事件处理程序的基础上额外做了不少工作:它不仅调用了注册的回调,还让 Angular 有机会去执行一遍变更检测、更新 UI。

支持浏览器异步 API

被“修补”以支持变更检测的常用浏览器机制,主要包括:

  • 所有浏览器事件(单击、鼠标悬停、按键等)
  • setTimeout()setInterval()
  • Ajax HTTP 请求

实际上,Zone.js 修补了更多浏览器 API,比如 Websockets,目的就是透明地触发 Angular 变更检测。

但这种机制也存在一个局限:如果因为某些原因,某个异步浏览器 API 不被 Zone.js 支持,那么变更检测就不会被触发。IndexedDB 的回调就是一个典型的例子。

默认的变更检测机制是如何工作的?

每个 Angular 组件都拥有自己的变更检测器,这些检测器在应用启动时就已经创建好了。来看一个例子:

@Component({
    selector: 'todo-item',
    template: `{{todo.owner.firstname}} - {{todo.description}}
       - completed: {{todo.completed}}`
})
export class TodoItem {
    @Input()
    todo:Todo;
    @Output()
    toggle = new EventEmitter();
    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 类里添加一些代码,在访问某个属性时触发断点即可。

断点命中后,遍历一下堆栈跟踪,就能看到变化检测的样貌:

\

这个方法乍一看可能有点怪异,所有变量名都很古怪。但仔细研究就会发现,它所做的事情其实非常简单:对于模板中使用的每个表达式,把该表达式中属性的当前值与它之前的值做一个比较。

如果前后的值不一致,就把isChanged设置为true。简单来说,它就是通过一个叫looseNotIdentical()的方法来比较数值。

嵌套对象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: `

` }) 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能带来更好的性能,但如果与可变对象一起使用,它的使用成本会非常高,很容易引入一些难以排查和复现的 bug。当然,也有办法能让OnPush用得更加顺手。

用 Immutable.js 简化 Angular 应用的构建

如果我们只用不可变对象和不可变列表来构建应用,那么就能在OnPush模式下放心地到处使用,不必担心遇到变更检测错误。因为对于不可变对象来说,修改数据的唯一方法就是创建一个新的不可变对象,然后替换掉之前的对象。使用不可变对象,可以保证:

  • 新的不可变对象一定会触发OnPush的变更检测
  • 不会因为忘记创建对象的新副本而意外引入 bug,因为修改数据的唯一方式就是创建新对象

实现不可变的一个好选择是 Immutable.js 这个库。它提供了不可变对象(Map)和不可变列表等构建应用的基本元素。

避免变更检测循环:生产与开发模式

Angular 变更检测的一个重要特性是,它不像 AngularJS 那样,而是强制采用单向数据流:当控制器上的数据更新时,变更检测运行并更新视图。

在 Angular 中如何触发变更检测循环?

一种方法是使用生命周期回调。比如,在 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 总是运行两次变更检测为代价的(第二次就是为了检测这种情况)。而在生产模式下,变更检测只运行一次。

打开/关闭变化检测,并手动触发它

在某些特殊场景下,我们确实想关闭变更检测。想象一下,有大量数据通过 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 非常相似:都是在浏览器事件发生前后比较模板表达式的值,看有没有变化。而且它对所有组件都这么做。但两者也有一些重要区别:

一方面,Angular 没有变化检测循环,也没有 AngularJS 里那个著名的“摘要循环”。这样一来,我们只需要看模板和控制器,就能推理出每个组件的行为。

另一个区别是,因为变更检测器的构建方式,Angular 检测组件变化的机制要快得多。

最后,与 AngularJS 不同,Angular 的变更检测机制是可以定制的。

来源:https://www.jb51.net/article/266242.htm
上一篇Angular中Observable常见问题与解决方法 下一篇Angular SSR服务端渲染原理与实战解析
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
HTML双英雄图精准居中与并排对齐实战指南
前端开发 · 2026-07-04

HTML双英雄图精准居中与并排对齐实战指南

本文详解如何使用CSS Flexbox将两个英雄图在页面中水平居中、等高对齐,并保持50px间距,解决justify-content align-items单独作用于子元素无效的问题。 想让两个视觉冲击力十足的英雄图在首页并排居中,是提升首屏吸引力的经典设计。但很多开发者都踩过同一个坑:直接在 `

Flexbox实现div水平垂直居中的方法
前端开发 · 2026-07-04

Flexbox实现div水平垂直居中的方法

使用 Flexbox 实现 div 的水平垂直居中,推荐在父容器上设置 display: flex,并配合 justify-content: center(控制主轴居中)与 align-items: center(控制交叉轴居中),同时确保父容器拥有明确高度,例如 min-height: 100vh

React循环中正确管理多个独立Modal实例的方法
前端开发 · 2026-07-04

React循环中正确管理多个独立Modal实例的方法

在 React 开发中,我们常常会遇到这样的场景:需要在一个列表循环里渲染多个弹窗(Modal)。如果处理不当,点击任何一个按钮,都会导致所有的弹窗同时打开或关闭,这显然不是我们想要的效果。问题的根源在于状态管理:当多个 Modal 实例共享同一份控制其显示隐藏的状态时,它们的行为就被捆绑在了一起。

鼠标滚动切换图片与7秒无操作自动轮播完整教程
前端开发 · 2026-07-04

鼠标滚动切换图片与7秒无操作自动轮播完整教程

本文介绍如何结合鼠标滚轮交互与定时器机制,实现图片在用户滚动时手动切换、7秒无操作后自动轮播的双重功能,并提供可复用、多实例支持的现代化 JavaScript 解决方案。 在网页开发中,图片轮播组件虽然常见,但许多实现方案在用户体验上仍存遗憾。例如,完全依赖用户滚动切换的轮播,当用户停止操作专注查看

输入新城市自动清除旧天气数据实现方法
前端开发 · 2026-07-04

输入新城市自动清除旧天气数据实现方法

本文详解如何借助 JavaScript 在用户切换查询城市时,自动清空先前展示的天气信息,避免新旧数据混杂叠加,从而优化单页应用的交互体验。 在基于 OpenWeather API 打造天气查询工具时,很多开发者都会遇到一个颇为棘手的小问题:用户查完一个城市后,紧接着输入另一个城市名称,页面上新旧天