Angular更改检测终极指南
项目地址:https://github.com/Mokkapps/angular-change-detection-demo
Angular 的两大宗旨是可预测和高效。框架需要组合状态和模板,以在 UI 上复制应用程序的状态:
如果状态发生任何更改,就必须更新视图。将 HTML 与我们的数据同步的机制被称为“更改检测”。每个前端框架都有对应的实现,例如 React 使用虚拟 DOM,Angular 使用更改检测等。我推荐大家阅读《JavaScript 框架中的更改及其检测》,这篇文章提供了关于这一主题的很不错的概述。
https://teropa.info/blog/2015/03/02/change-and-its-detection-in-javascript-frameworks.html
更改检测:数据更改后更新视图(DOM)的过程。
作为开发人员,大多数时候我们不需要关心更改检测,除非我们需要优化应用程序的性能。如果处理不当,更改检测会降低大型应用程序的性能。
开发人员 更新应用程序模型; Angular 通过重新渲染视图来同步视图中更新的模型。
开发人员更新数据模型,例如更新组件绑定; Angular 检测到了更改; 更改检测从上到下检查组件树中的 每个 组件,以查看对应的模型是否已更改; 如果有新值,它将更新组件的视图(DOM)。
以下 GIF 以简化的形式演示了这一过程:
这张图显示了一个 Angular 组件树及其在应用程序引导过程中为每个组件创建的更改检测器(CD)。检测器会对比属性的当前值与先前值,如果值已更改,它会将 isChanged 设置为 true。可以看一下框架代码中的实现,实质上就是一个 === 对比,对 NaN 有特殊处理。
https://github.com/angular/angular/blob/885f1af509eb7d9ee049349a2fe5565282fbfefb/packages/core/src/util/comparison.ts#L13
更改检测不执行深度对象比较,它只对比模板使用属性的先前值和当前值。
它在开始时是稳定的; 任务在区域中运行时,它会变得不稳定; 任务完成后,它会再次稳定下来。
Angular 在启动时修补了几个浏览器的底层 API,以便检测应用程序中的更改。这是使用 zone.js 完成的,其修补了 EventEmitter、DOM 事件侦听器、XMLHttpRequest 和 Node.js 中的 fs 等 API。
任何浏览器事件(单击、键入等); setInterval() 和 setTimeout(); 通过 XMLHttpRequest 的 HTTP 请求。
Angular 将自己的区域称为 NgZone。仅存在一个 NgZone,并且仅针对此区域中触发的异步操作触发更改检测。
默认情况下,如果模板值已更改,Angular 更改检测将 从上至下检查所有组件。
Angular 对每个组件进行更改检测的速度非常快,因为它可以使用内联缓存在几毫秒内执行数千次检查,其中内联缓存可生成对 VM 优化的代码。
如果你想了解有关这个主题的更深入的说明,建议你观看 Victor Savkin 的演讲:重塑更改检测。
https://www.youtube.com/watch?v=jvKGQSFQf10
尽管 Angular 在后台进行了大量优化,但在大型应用程序上性能可能仍会下降。在下一章节中,你将学习如何使用不同的更改检测策略来主动改善 Angular 性能。
Default OnPush
我们来具体研究一下这两种策略。
在默认情况下,Angular 使用 ChangeDetectionStrategy.Default 更改检测策略。每当事件触发更改检测(例如用户事件、计时器、XHR、promise 等)时,这个默认策略都会从上到下检查组件树中的每一个组件。这种不对组件依赖项做任何假设的保守检查方法被称为 脏检查。它可能会对包含许多组件的大型应用程序的性能产生负面影响。
@Component({
selector: 'hero-card',
changeDetection: ChangeDetectionStrategy.OnPush,
template: ...
})
export class HeroCard {
...
}
这种更改检测策略可以跳过对这个组件及其所有子组件的非必要检查。
下面这张 GIF 演示了使用 OnPush 更改检测策略跳过组件树的某些部分:
输入引用已更改; 该组件或其子组件之一触发了一个事件处理程序; 更改检测是手动触发的; 通过异步管道链接到模板的一个可观察对象发出了一个新值。
我们来仔细看看这些事件。
在默认的更改检测策略中,每当 @Input() 数据被更改或修改时,Angular 都会运行更改检测器。使用 OnPush 策略时,只有当一个 新引用 被作为 @Input() 值传递时,才会触发更改检测器。
数值、字符串、布尔值、null 和 undefined 之类的原始类型按值传递。对象和数组也按值传递,但是修改对象属性或数组条目不会创建新的引用,因此不会触发 OnPush 组件的更改检测。要触发更改检测器,你需要传递一个新的对象或数组引用。
你可以使用这个简单的演示来测试这一行为:
使用 ChangeDetectionStrategy.Default 修改 HeroCardComponent 的 age; 带有 ChangeDetectionStrategy.OnPush 的 HeroCardOnPushComponent 不能反映更改的 age(组件周围会显示红色边框); 在“Modify Heroes”面板中单击“Create new object reference”; 现在更改检测会检查带有 ChangeDetectionStrategy.OnPush 的 HeroCardOnPushComponent。
每次更改都会触发 OnPush 更改检测; 我们不会忘记创建新的对象引用,否则会导致一些错误。
Immutable.js 是一个不错的选择,这个库为对象(Map)和列表(List)提供了持久的不可变数据结构。通过 npm 安装这个库后,我们就有了类型定义,这样就可以在 IDE 中使用类型泛型、错误检测和自动完成功能。
库:https://www.npmjs.com/package/immutable
如果 OnPush 组件或其子组件之一触发了一个事件处理程序(如单击按钮),将触发更改检测(针对组件树中的所有组件)。
setTimeout setInterval Promise.resolve().then()(当然 Promise.reject().then() 也是一样) this.http.get('...').subscribe()(也就是任何 RxJS 可观察的订阅)
你可以使用这个简单的演示测试此行为:
在使用 ChangeDetectionStrategy.OnPush 的 HeroCardOnPushComponent 中单击“Change Age”按钮; 可以看到更改检测被触发,并检查所有组件。
ChangeDetectorRef 上的 detectChanges(),它会在这个视图及其子级上运行更改检测,并遵循已有的更改检测策略。它可以与 detach() 结合使用,以实现本地更改检测检查。 ApplicationRef.tick(),它会依照组件的更改检测策略,触发整个应用程序的更改检测。 ChangeDetectorRef 上的 markForCheck()不会 触发更改检测,但会将所有 OnPush 祖先标记为要检查一次,在当前或下一个更改检测周期中检查。即使已标记的组件正在使用 OnPush 策略,也将运行更改检测。
手动运行更改检测不是什么 hack 手段,但你只能在合理的情况下使用它。
下图展示了不同的 ChangeDetectorRef 方法:
你可以在演示中使用“DC”(detectChanges())和“MFC”(markForCheck())按钮来测试其中的一些动作。
https://angular-change-detection-demo.netlify.com/simple-demo
内置的 AsyncPipe 订阅一个可观察对象,并返回它发出的最新值。
每次发出新值时,AsyncPipe 内部都会调用 markForCheck,请参见其源代码:
private _updateLatestValue(async: any, value: Object): void {
if (async === this._obj) {
this._latestValue = value;
this._ref.markForCheck();
}
}
如图所示,AsyncPipe 使用 OnPush 更改检测策略自动运行。因此建议尽量多用它,以便将来从默认更改检测策略切换到 OnPush 上。
你可以在异步演示中看到这种行为。
https://angular-change-detection-demo.netlify.com/async-pipe-demo
<mat-card-title>{{ (hero$ | async).name }}</mat-card-title>
hero$: Observable<Hero>;
ngOnInit(): void {
this.hero$ = interval(1000).pipe(
startWith(createHero()),
map(() => createHero())
);
}
<mat-card-title>{{ hero.name }}</mat-card-title>
hero: Hero = createHero();
ngOnInit(): void {
interval(1000)
.pipe(map(() => createHero()))
.subscribe(() => {
this.hero = createHero();
console.log(
'HeroCardAsyncPipeComponent new hero without AsyncPipe: ',
this.hero
);
});
}
如你所见,没有 AsyncPipe 的实现不会触发更改检测,因此我们需要为可观察对象发出的每个新事件手动调用 detectChanges()。
Angular 有一种检测更改检测循环的机制。在开发模式下,框架运行两次更改检测,以检查自首次运行以来该值是否已更改。在生产模式下,更改检测仅运行一次以获得更好的性能。
我在 ExpressionChangedAfterCheckedError 演示中强加了这个错误,打开浏览器控制台就能看到:
https://angular-change-detection-demo.netlify.com/expression-changed-demo
ngAfterViewInit(): void {
this.hero.name = 'Another name which triggers ExpressionChangedAfterItHasBeenCheckedError';
}
要搞清楚为什么会导致错误,我们需要查看更改检测运行期间的各个步骤:
如你所见,在渲染了当前视图的 DOM 更新之后,将调用 AfterViewInit 生命周期 hook。如果我们更改这个 hook 中的值,它在第二次更改检测中将具有不同的值(如上所述,第二次检测在开发模式下是自动触发的),因此 Angular 将抛出 ExpressionChangedAfterCheckedError。
我强烈建议你阅读 Max Koretskyi 撰写的《Angular 更改检测全面解析》,它详细探讨了著名的 ExpressionChangedAfterCheckedError 的底层实现和用例。
https://blog.angularindepth.com/everything-you-need-to-know-about-change-detection-in-angular-8006c51d206f
constructor(private ngZone: NgZone) {}
runWithoutChangeDetection() {
this.ngZone.runOutsideAngular(() => {
// 后面的 setTimeout 不会触发更改检测
setTimeout(() => doStuff(), 1000);
});
}
这个简单的演示提供了一个按钮,可以触发一个 Angular 区域之外的动作:
你能看到这个动作已在控制台中记录了下来,但是 HeroCard 组件没有被检查,意味着它们的边框不会变成红色。
这个机制对由 Protractor 运行的端到端测试很有用,尤其是在测试中使用 browser.waitForAngular 的情况下。将每个命令发送到浏览器后,Protractor 将等待到区域变得稳定为止。如果使用 setInterval,区域将永远不会稳定,并且测试可能会超时。
RxJS 可观察对象可能会遇到相同的问题,但你需要按照 Zone.js 对非标准 API 的支持文档所述,将修补版本添加到 polyfill.ts 中:
import 'zone.js/dist/zone'; // 用 Angular CLI 加入进来.
import 'zone.js/dist/zone-patch-rxjs'; // 导入 RxJS 补丁来确保 RxJS 运行在正确的区域中
如果没有这个修补程序,你可以在 ngZone.runOutsideAngular 内部运行可观察对象的代码,但它仍会作为在 NgZone 内部的任务来运行。
constructor(private ref: ChangeDetectorRef) {
ref.detach(); // 停用更改检测
setInterval(() => {
this.ref.detectChanges(); // 手动触发更改检测
}, 10 * 1000);
}
在 Angular 应用程序的引导过程中,也可以完全停用 Zone.js。这意味着自动更改检测已完全停用,我们需要手动触发用户界面更改,例如调用 ChangeDetectorRef.detectChanges()。
import 'zone.js/dist/zone'; // Included with Angular CLI.
platformBrowserDynamic().bootstrapModule(AppModule, {
ngZone: 'noop';
}).catch(err => console.log(err));
有关停用 Zone.js 的更多细节,请参见文章《没有 Zone.Js 的 Angular Elements》。
https://www.softwarearchitekt.at/aktuelles/angular-elements-part-iii/
默认情况下,Angular 9 将使用 Angular 的下一代编译和渲染管道 Ivy。从 Angular 8 开始,你可以选择使用 Ivy 的预览版本,并帮助其开发和改进。
Angular 团队将确保新的渲染引擎仍以正确的顺序处理所有框架的生命周期 hooks,以便更改检测能正常工作。因此,你还是会在应用程序中看到相同的 ExpressionChangedAfterCheckedError。
Max Koretskyi 在这篇文章中写道:
https://indepth.dev/ivy-engine-in-angular-first-in-depth-look-at-compilation-runtime-and-change-detection/
如你所见,所有熟悉的操作都在。但是操作顺序似乎已经改变了。例如,现在 Angular 会先检查子组件,然后才检查嵌入式视图。由于目前没有编译器可以生成合适的输出来验证我的假设,因此我还不确定。
你可以在本文末尾的“推荐文章”部分中找到另外两篇与 Ivy 相关的有趣文章。
Angular 更改检测是一种强大的框架机制,可确保我们的 UI 以可预测和高效的方式表示我们的数据。可以肯定地说,更改检测适用于大多数应用程序,尤其是包含的组件少于 50 个的应用。
你收到一个 ExpressionChangedAfterCheckedError,并需要解决它。 你需要提高应用程序性能。
希望本文能帮助你更好地了解 Angular 的更改检测。请随意使用我的演示项目来尝试不同的更改检测策略。
https://github.com/Mokkapps/angular-change-detection-demo
推荐文章
Angular 更改检测——它如何运作? https://blog.angular-university.io/how-does-angular-2-change-detection-really-work/ Angular OnPush 更改检测和组件设计——避免常见的陷阱 https://blog.angular-university.io/onpush-change-detection-how-it-works/ Angular onPush 更改检测策略综合指南 https://netbasal.com/a-comprehensive-guide-to-angular-onpush-change-detection-strategy-5bac493074a4 Angular 更改检测说明 https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html -
Angular Ivy 更改检测执行:你准备好了吗?
https://blog.angularindepth.com/angular-ivy-change-detection-execution-are-you-prepared-ab68d4231f2c 了解 Angular Ivy:增量 DOM 和虚拟 DOM https://blog.nrwl.io/understanding-angular-ivy-incremental-dom-and-virtual-dom-243be844bf36
原文链接:https://www.mokkapps.de/blog/the-last-guide-for-angular-change-detection-you-will-ever-need/
- 【LEDE】树莓派上玩LEDE终极指南-3-更改时区
- 【OpenCV入门指南】第十三篇 人脸检测
- 【LEDE】树莓派上玩LEDE终极指南-98-Node.js和NPM在LEDE上存在的问题和解决方案
- 前端代码检测利器——eslint for sublime安装指南
- [译]移动API安全终极指南
- 如何编写更好的SQL查询:终极指南-第一部分
- WINXP终极瘦身、优化、封装全攻略 万能WinXP Ghost镜像制作指南
- Webpack 入门指南 - 3. Hello, Angular2!
- 如何编写更好的SQL查询:终极指南-第二部分
- 如何编写更好的SQL查询:终极指南-第二部分
- HTML5终极指南:2012年的14个预测
- WordPress SEO ☞ WordPress网站终极优化指南
- 【OpenCV入门指南】第七篇 线段检测与圆检测
- Angular2语法快速指南
- 【OpenCV入门指南】第十三篇 人脸检测
- [Web开发] IE文档模式终极指南
- Android APP终极瘦身指南
- 提升网站访问速度的终极指南&lt;转&gt;
- Android APP终极瘦身指南
- linux开发终极指南