我是靠谱客的博主 苹果奇异果,最近开发中收集的这篇文章主要介绍带你详细了解Angular中的变更检测,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

本篇文章带你详细了解一下Angular中的变更检测,给大家介绍一下Angular 的 DOM 更新机制、变更检测能解决什么问题等。

通过这篇的文章,可以帮助你收获这些知识:

  • Angular 的 DOM 更新是如何做到的?【相关教程推荐:《angular教程》】
  • 变更检测解决了什么问题?
  • 更深入了解变更检测,熟悉它在 Angular 源码中的定义以及在 ng-zorro 中的使用
  • 以及一点个人感悟

一、Angular 的 DOM 更新机制

我们先来看一个最简单地 demo

1.gif

在按钮被点击时,我们改变了组件的 name 属性,就在一瞬间 DOM 中也显示出了改变后的值,这似乎有些“神奇”。

2.gif

如果紧跟着元素更改的语句之后打印出真实 DOM 中的 innterText,却发现仍然还是旧的值,可是明明视图中的值已经改变了,这两段代码中到底发生了什么呢?如果你也对此也疑惑不已,那么就和我一起来揭晓这个答案。

我们仔细回忆下刚刚发生的事情:

  • 点击按钮

  • 值改变

如果使用原生 JS 来编写这段代码,那么点击按钮后的视图肯定不会发生改变,而在 Angular 中却让视图发生了改变。如果你对 Angular 有稍微深入的了解,就会知道一个叫做 zone.js 的库,仔细翻看就会发现,zone.js 对所有可能发生值改变的事件做了一层处理,比如:

  • Events:click,mouseover,mouseout,keyup,keydown 等所有的浏览器事件
  • Timer:setTimeout,setInterval
  • XHR:各类请求等

Angular 还为我们提供了禁用 zone.js 的方法。

3.png

4.gif

禁用 zone 后,当我们再次点击按钮时,视图未更新。

带着好奇心,我们找到 Angular 源码中视图更新的关键代码

5.png

这一次我们手动在代码调用这个方法。

6.png

7-1.gif

果然和预料中的一样!视图更新了,更让人惊喜是,打印出来的 innerText 也更新了!

到这里,我们得出了一个结论,DOM 的更新依赖于 tick() 的触发,zone.js 帮助开发者省去了手动触发的操作

好了,小试牛刀之后,接下里我们就来仔细探究 Angular 视图更新的背后到底发生了什么。

二、窥探变更检测的秘密

1.从一个常见的 Error 说起

我们先来看这样一处错误,在 child 组件的 ngOnInit 中更改了父组件 parent 的 name 值,结果出现了大家一定都遇到过的错误信息

8.png

9.png

可是这样写并不是每次都会报错,例如我们去掉子组件 child 的输入属性,刷新一下,发现同样的代码却可以运行,父组件的 name 可以被正常更改。

10.png

emmm... 陷入沉思...

也许你和刚开始学习 Angular 时的我一样,在 stackoverflow 里搜索这个问题,复制了个自己也不知道为什么能起作用的代码就直接粘贴了上去,后面再遇到这个问题时,继续在 stackoverflow 里搜索和复制粘贴,如此反复...

随着时间的推移,精通各种 CRUD 的你越来越不满足于这种面向 stackoverflow 编程的自己,开始在社区、文档、论坛上不停的查找问题的答案,但是看完他们的回答和文章,好像只知道了有个叫做变更检测的东西,但是具体是怎么导致了这个 bug ,却支支吾吾的说不太清楚,如果你也和我一样对上述经历深有体会,那么就继续向下探寻真相吧!

2.说了半天的变更检测到底是什么?

当我们在 model 中改变数据时,框架层需要知道:

  • model 哪里发生了改变
  • view 中哪里需要更新

React 中的 Virtual Dom 大家一定都不陌生,React 通过对比 DOM 的新状态与旧状态来决定更新哪一部分 dom,而不是更新所有的 dom,这也是 Angular 中变更检测(change detection)的异曲同工之处。

整个 Angular 应用是个组件树,不可能任意一个组件中的改变都触发所有组件的更新,这样效率太低也太耗时,例如用户更改了某个 button 的状态,那么最理想的做法是只更新这个 button 的样式或文字,而不是整个应用全部更新一遍,变更检测的目的也就是为此。

默认情况下(ChangeDetectionStrategy.Default),父组件的变更检测发生时,子组件也会触发变更检测。

11.png

(CD 即为 changeDetection )

每次变更检测时,都会比较新旧状态,如果两次变更检测(开发环境下)的结果不一致就会报错,例如:

Expression has changed after it was checked

这也就解释了为什么在子组件中更改了父组件的值会报错。

但是!在前面的两个例子中我们都在子组件中更改了父组件的值,只有第一个报错,第二个是可以正常更新的,如果你也同样很疑惑这中间真正的差异点在哪里,那么接着往下阅读吧~

3.问题的关键 — 检测顺序 Detection Sequence

先上结论:

  • 更新所有子组件的绑定属性

  • 调用所有子组件的 OnChanges,OnInit,DoCheck,AfterContentInit 生命周期钩子

  • 更新当前组件的 DOM

  • 子组件触发变更检测

  • 调用所有子组件的的 AfterViewInit 的生命周期钩子

这里我们不关注于太细的细节(不用好奇为什么是这样的顺序,只要记住 Angular 里就是这样设定的就可以了,如果有大佬想谈谈 Angular 在这部分的设计思想,欢迎在评论区留言探讨~)

第一个例子中,父组件 parent 给子组件 child 传入了输入属性 name,且子组件在 ngOnInit 中更新了父组件的 name 属性,也就是说这段代码**违背了检测顺序(**在顺序的第二步中操作了第一步)!

<p>{{ name }}<p>
<child [name]="name"></child>
登录后复制

而在第二个例子中,就算子组件在 ngOnInit 中也更新了父组件的 name 属性,但是由于父组件parent 中没有给子组件 child 绑定输入属性 name,不会出现与违背变更检测队列顺序的情况,所以就可以正常运行。

<p>{{ name }}<p>
<child></child>
登录后复制

这个时候再去看看 stackoverflow 上的高赞回答 是不是就清晰明了很多,按照上述的检测顺序,我们会发现只要父组件中对子组件做了属性绑定,不管是在 OnChanges,OnInit,DoCheck,AfterContentInit 和 AfterViewInit 中的任意一个声明周期钩子中执行下述代码都会报错。

this.parentCmpt.name = 'child'
登录后复制

好了,到这里我们已经明白了这种错误发生的真正原因,但是我还是要提醒一下,这种错误只会在开发环境下触发,生产环境下会调用 enableProdMode() ,变更检测次数会从 2 降到 1,这部分在 Angular 源码当中也有描述。

12.png

当然你不能因为这个 bug 就强制在开发环境下使用生产模式...

4.大家常说的 ChangeDetectionStrategy.OnPush 又是什么?

ChangeDetectionStrategy 默认为 Default,也就是父组件的 CD 会触发子组件的 CD,但是很显然有些情况下我们可以自行判断出某些子组件在父组件 CD 时并不用触发,而 OnPush

则是 Angular 为开发者提供的一便捷操作方式。

13.png

用动图来表示就是:查看链接

14.gif

知名的 Angular 开源组件库 ng-zorro 就使用了大量的 OnPush 策略,这也是 Angular 性能优化的方法之一。

15.png

三、再深入了解一些

Angular 给每个组件都关联了一份组件视图,通过 ChangeDetectorRef 可以拿到相关联的视图,在定义中我们可以看到:

export declare abstract class ChangeDetectorRef {
    abstract checkNoChanges(): void;
    abstract detach(): void;
    abstract detectChanges(): void;
    abstract markForCheck(): void;
    abstract reattach(): void;
}
登录后复制

1.detach 和 reattach

观察下面的动图,被 detached 的组件将不会被检查变更。

16.gif

reattach 则可以让被 detached 的组件重新可以被检测到变更。

2.markForCheck

reattach 只会重新启用对当前组件的变更检测,但是如果父组件没有启动变更检测,那么 reattach 并不会起作用,而 markForCheck 可以很好地解决这个问题。

这一点在 ng-zorro 的源码中可以了解一二。

例如在 nz-anchor 组件中更改 nz-anchor-link 组件的 active 属性时,由于本身 ChangeDetectionStrategyOnPush ,那么就需要激活 markForCheck 来重新启用检测。具体写法可以查看 github 中的源代码。

  • nz-anchor.component.ts
  • nz-anchor-link.component.ts

用动图来展示则是这样,注意观察设置了 MFC 的前后变化

17.gif

3.detectChanges

这个方法如同字面意思一样很好理解,就是触发一次变更检测啦,还记得本文中的第一个例子吗,我们不手动触发 tick() ,而是触发 detechtChanges() 也是可以达到效果的。

18.png

19.gif

四、最后

到这里,我相信大家已经基本弄明白了 Angular 变更检测,如果有任何疑问,欢迎在评论区交流讨论~

在撰写这篇文章时,笔者参(fu)考(zhi)了大量的社区文章和讨论,一方面是感慨如此重要的概念在 Angular 中文社区中却只有零星几篇相关介绍的文章,另一方面是看到了虽然国内 Angular 开发者虽然数量远少于 React 和 Vue,却依然非常热情的贡献自己的知识和见解来为 Angular 中文社区添砖加瓦,作为已使用 Angular 半年多的开发者,深深感受到 Google 的工程美学。

大而全且不失优雅,是笔者对 Angular 这款 Web 框架的最大感受,感谢开源社区中的各位开发者们~

对于文中描述错误的地方,还望大佬们批评斧正~

最后

以上就是苹果奇异果为你收集整理的带你详细了解Angular中的变更检测的全部内容,希望文章能够帮你解决带你详细了解Angular中的变更检测所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(79)

评论列表共有 0 条评论

立即
投稿
返回
顶部