什么是变更检测(Change Detection)?
变更检测的概念
组件内的数据状态变化以后,需要对应更新视图。这种将视图和数据同步的机制,就叫变化检测。【相关教程推荐:《angular教程》】
变更检测的触发时机
只要发生了异步操作(Events, Timer, XHR),Angular 就会认为有状态可能发生变化了,然后就会进行变更检测。
- Events::click,mouseover,mouseout,keyup,keydown 等浏览器事件;
- Timer:setTimeout/setInterval;
- XHR:各类请求等。
既然都是对异步操作进行变更检测,那么Angular是如何订阅异步请求,进行变更检测的呢?
这里介绍下NgZone
以及它的fork对象Zone.js
。
Zone.js
用于封装和拦截浏览器中的异步活动、它还提供 异步生命周期的钩子 和 统一的异步错误处理机制。
Zone.js
是通过 Monkey Patching(猴子补丁) 的方式来对浏览器中的常见方法和元素进行拦截,例如 setTimeout
和 HTMLElement.prototype.onclick
。Angular 在启动时会利用 Zone.js
修补几个低级浏览器 API,从而实现异步事件的捕获,并在捕获时间后调用变更检测。
Angular通过forkZone.js
并拓展出一个自己的区域NgZone
,让应用中的所有异步操作都会运行在这个区域中。
Angular的变更检测如何工作的?
Angualr会为每一个组件生成一个变化监测器changeDetector
,记录组件的变化状态。
我们在创建了一个Angular 应用后,Angular 会同时创建一个 ApplicationRef
的实例,这个实例代表的就是我们当前创建的这个 Angular 应用的实例。 ApplicationRef
创建的同时,会订阅 ngZone 中的 onMicrotaskEmpty 事件,在所有的微任务完成后调用所有的视图的detectChanges()
来执行变更检测。
变更检测的执行顺序
更新所有子子组件绑定的属性
调用所有子组件生命周期的钩子 OnChanges, OnInit, DoCheck,AfterContentInit
更新当前组件的DOM
调用子组件的变更检测
调用所有子组件的生命周期钩子 ngAfterViewInit
举个栗子,我们在开发模式的时候可能会遇到这种报错:
这是由于变更检测遵循从根组件开始,从上到下,执行每个组件的变更检测,直到最后一个组件达到稳定状态。而在下一次变更检测之前,子孙组件都不允许去修改父组件里的属性。
情况1 在开发模式下,Angular会进行二次检测 (生产环境下调用enableProdMode()
,检测次数会降为1)。一旦我们在 第4步 完成后,在子孙组件里修改父组件的属性,那么,Angular在执行第二次检测的时候,发现两次的值不一致,就会出现上述错误。
情况2 只要父组件对子组件做了属性绑定,那么不管是在OnChanges,OnInit,DoCheck,AfterContentInit 和 AfterViewInit 中的任意一个生命周期钩子中执行下述代码,也会报错。
// #parent {{data}} <child [data]="data"></child> // in child component ts, execute: this.parent.data = 'new Value';
变更检测的执行策略
Default 策略
每次事件触发变化检测(如用户事件、计时器、XHR、promise 等)时,此默认策略都会从上到下检查组件树中的每个组件。这种对组件的依赖关系不做任何假设的保守检查方式称为 脏检查,这种策略在我们应用组件过多时会对我们的应用产生性能的影响。
OnPush 策略
修改组件装饰器的
changeDetection
,设置为 OnPush 策略后,Angular 每次触发变化检测后会跳过该组件和该组件的所以子组件变化检测。在
OnPush
策略下,只有以下这几种情况才会触发组件的变化检测:- 输入值(@Input)更改(入input的值必须是一个新的引用)
- 当前组件或子组件之一触发了事件 (但在onPush策略中,以下操作不会触发变更检测)
- setTimeout()
- setInterval()
- Promise.resolve().then()
- this.http.get('...').subscribe()
- 手动触发变更检测(每个组件都会关联一个组件视图ChangeDetectorRef)
- detectChanges(): 它会触发当前组件和子组件的变化检测
- markForCheck(): 它不会触发变化检测,但是会把当前的OnPush组件和所以的父组件为OnPush的组件 标记为需要检测状态 ,在当前或者下一个变化检测周期进行检测
- ApplicationRef.tick() : 它会根据组件的变化检测策略,触发整个应用程序的更改检测
- async pipe
对于Angular的变更检测如何优化?
由于组件默认执行 Default策略 ,任何异步操作都会触发整个组件数从上到下的检查。即使Angular团队不断提升性能,可以在毫秒内完成上百次检测,但是当应用拓展至百上千个组件组成时,庞大的组件树对应的变更检测也会达到性能瓶颈。
此时,我们就需要开始分析并减少不必要的检测次数。
如何减少检测次数
区域污染(Zone Pollution)
一般我们在生命周期钩子里使用第三方库,比如chart类库初始化,会自带requestAnimationRequest/setTimeout/addEventListener,我们可以将初始化方法写入
NgZone
的runOutsideAngular
方法中。
OnPush 策略
对于不涉及更新操作的视图可以剥离出组件,使用onPush策略,以通知更新的方式刷新视图(见上方 变更检测的执行策略 部分)。
用 pure pipe 代替 {{function(data)}}
在html文件内,
{{function(data)}}
的写法会导致每次变更检测发生时,所有数值都会重新被计算。(?:当一个1000条的列表,你只修改了其中一条数据,但另外另外999条无需更新的数据也会被重新运算。)此时,我们可以使用pipe的方式,只有变更的值会触发运算,更新部分视图。
面对大量数据的渲染,选择虚拟滚动/分页请求数据
以上4点解决方案,来源于Angular团队的视频介绍,视频中以Angular devtool运算次数,来分析问题解决问题。所以,如果你的Angular是9+,请继续看下去吧,如何安装Angular devtool和运行。
插件:Angular devtool使用介绍
- Angular 9+, 支持Ivy。
- Guide和下载地址
- 保证运行环境为开发环境
// environment.dev.ts ... production: false ...
- angular.json > dev配置项 >
"optimization": false
projects > your-project-name > architect > build > configurations > dev > "optimization": false
更多编程相关知识,请访问:编程教学!!
以上就是浅析Angular变更检测机制,聊聊如何进行性能优化?的详细内容,更多请关注自由互联其它相关文章!