当前位置: 首页
前端开发
Angular变更检测机制实现原理详解

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

热心网友 时间:2026-06-16
转载

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

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
如何用拖放在Flask看板应用中更新数据库状态

如何用拖放在Flask看板应用中更新数据库状态

在Flask看板应用中,通过HTML5拖放API结合前端fetch异步POST请求,将拖拽任务的task_id和target_state发送至后端路由;后端接收JSON后更新数据库并提交事务,实现跨列拖拽时实时更新状态。需注意将事件绑定到容器而非子元素,并在ondragover中阻止默认行为以确保drop触发。

时间:2026-07-09 07:01
如何用CSS :has()选择器快速实现悬停span显示相邻div

如何用CSS :has()选择器快速实现悬停span显示相邻div

利用CSS的:has()伪类选择器,通过` bar:has(> baz:hover)+ qux`实现悬停内联元素时显示相邻块级容器,解决了传统相邻兄弟选择器无法跨层级匹配的局限,可用于悬停图标显示详情框等交互场景。需注意浏览器兼容性和性能优化,避免过度使用影响渲染效率。

时间:2026-07-09 07:01
display:block居中段落文本的响应式实现方法

display:block居中段落文本的响应式实现方法

通过`display:block`与`max-width`限制宽度,配合`margin:auto`使段落块水平居中并实现响应式布局;同时用CSS统一控制段落间距,避免脱离文档流,提升可读性与维护性。

时间:2026-07-09 07:01
JavaScript代码去重:高效避免重复编写的技巧

JavaScript代码去重:高效避免重复编写的技巧

将重复代码中的变化点提取为参数,利用ES6模板字符串动态生成选择器,可消除重复、保持一致性。注意参数安全性,必要时进行白名单校验;批量化场景可扩展为数组形式;现代框架中更推荐数据驱动或组件化封装替代手动选择器。

时间:2026-07-09 07:00
使用容器查询单位cqw实现表单输入框相对于祖父容器百分比宽度

使用容器查询单位cqw实现表单输入框相对于祖父容器百分比宽度

利用CSS容器查询单位cqw,表单输入框宽度可直接基于祖父容器计算,绕过中间包装器尺寸干扰,实现简洁响应式布局,无需硬编码或JavaScript,显著降低维护成本,提升嵌套结构下的布局灵活性。

时间:2026-07-09 07:00
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全
  • 热门数据榜