代码位置
- Angular 源码
- 版本
15.0.0-next.0
- commit
bea953a5cfe4cbd507fdfd67073000251d614c9f
本篇文章均以这个版本的源码作为讲解 未来官方可能会提供一个无
ngZone
的方案(猜测是静态解析),到时候本篇文章可能就不太准确,需要诸位自行理解
前言
- 只有异步,会产生变更
- 检测一直存在,我们能决定的只是是否允许其被检测
- 与其叫
变更检测
,不如叫检测 变更
检测
- 就目前来讲,检测分为
自动检测
和手动检测
自动检测
- 自动检测永远存在,只要运行着
ngZone
,那么自动检测会永远开启即只要引入了
zone.js
,并且未更改启动时的ngZone
选项 - 目前
正常的开发项目
的自动检测存在于ApplicationRef
的构造函数中,监听微任务
为空时,会在angular zone
内触发自动检测
代码位置
packages\core\src\application_ref.ts:766
自动检测永远在angular zone
内 - 使用
Zone
的run
,runGuarded
,runTask
也会触发自动检测
具体解释在下面
什么是微任务为空
?
packages\core\src\zone\ng_zone.ts:395
- 宏观的讲,就是某一时间段内异步任务全部执行完成后,
微任务为空
- 完整的讲,1. 某一时间段内没有异步任务,2. 某一时间段内没有使用
Zone.run
,Zone.runTask
,Zone.runGuarded
这里的
run
,runGuarded
,runTask
包括NgZone
和Zone
两种使用方法,只要调用,在执行完之前,微任务不为空
- 微任务为空其实并不准确,但是函数命名是这么命名的
onMicrotaskEmpty
,所以目前就以这个名字为准 angular zone
内不止监听微任务(Promise)的执行情况,对宏任务,事件也会有监听,具体以补丁为准,基本上就是我们认知的那三种都包含在内不要问
async await
咋监听,监听不了,所以现在的打包 target 都是降级到 es2015 进行打包的(其实官方可以改成 es2016…因为async await
是 2017 添加的…)
手动检测
- 通过依赖注入获得
ChangeDetectorRef
来进行手动检测
当前组件的检测
constructor(private cd: ChangeDetectorRef) { cd.detectChanges() }
ApplicationRef.tick
触发全局的手动检测
默认的
自动检测
也是使用的这个方法angular zone
与<root> zone
自动检测
是如何实现的?简单的说就是所有在angular zone
中的异步操作和主动在 zone 空间内运行的代码都会被拦截,计数,然后当计数为 0(即执行完时),会触发自动检测
<root> zone
中的任何操作都不会被拦截,所以任何操作也不会触发自动检测
<root>
也就是全局的 zone
优化
缩小自动检测范围
packages\core\src\application_ref.ts:1001
ApplicationRef.tick
packages\core\src\render3\instructions\shared.ts:1643
refreshComponent
- 对于不使用的组件,手动进行视图分离,即逻辑和视图不再关联,这样检测的时候也不会处理这个组件.
markForCheck
标记 dirty,对于不标记的为 dirty 的,不会进行检测只对当前组件及祖先标记为 dirty,等待自动检测
// 分离视图
viewRef.detach();
// 在 @Component 中设置
changeDetection: ChangeDetectionStrategy.OnPush,
// 使用 ChangeDetectorRef 进行手动标记
constructor(private cd: ChangeDetectorRef) {}
xxx(){
this.cd.markForCheck()
this.cd.detectChanges()
}
将异步移出angular zone
- 将部分异步逻辑(微任务,宏任务,事件)转到
<root> zone
中执行,因为不停的触发会导致自动检测
不断的执行 - 一般情况下,异步+markForCheck 就可以触发
自动检测
,但是有时候出现在<root> zone
时,就需要detectChanges
手动检测
与自动检测
的区别
- 手动检测触发点在于这个组件的检测,也就是只检测这个组件和这个组件的后代
- 自动检测就是检测所有连接的根组件和他们的后代
正常使用中那些操作会进行检测
所有的异步操作
- 微任务,宏任务都会自动触发
- ng包裹事件(即html中的()和ts中的@HostListener)也会触发,但是还会将组件标记为dirty保证
OnPush
时候触发packages\core\src\render3\instructions\listener.ts:244
- 普通的元素事件监听也能触发,但是不会自动标记dirty
设置输入值(@Input)
packages\core\src\render3\instructions\shared.ts:969
elementPropertyInternal
- 父组件在触发了
检测
,进行检测时,假如值的变化传递到了子组件(@Input),那么会标记子组件为dirty同时进行检测 - 父组件是
OnPush
,子组件也OnPush
,并且父组件没有触发检测
,那么父组件的值变化不会通过@Input 传给子组件
什么叫父组件触发检测
- 父组件在异步回调中使用了
markForCheck
- 父组件使用了
detectChanges
- 虽然父组件在异步回调外使用了
markForCheck
,但是其他组件恰好触发了一次自动检测
手动实例化后设置输入值(相当于设置@Input)
packages\core\src\render3\component_ref.ts:283
setInput
- 这个应该是最近加的特性,以前没有
单元测试
packages\core\testing\src\component_fixture.ts:67
- provider 中
ComponentFixtureAutoDetect
设置为 true 会开启自动检测
- 如果不设置的话,就是只有手动检测
- 简单的说就是单元测试默认不开启
自动检测
web component
- 与
正常的开发项目
相同,但多了两个检测监听位置 packages\elements\src\component-factory-strategy.ts
- 第一次连接的时候调用一次检测
connectedCallback
- 设置输入属性的时候调用一次检测
初始化输入属性可以是元素上的,也就是
element
上定义的 先读取初始设置值,发现没有实例化组件,保存初始化输入值
实例化组件,初始化输入值
,自动检测
如果在这之后触发attributeChangedCallback
,自动检测
检测的时间
- 与默认的项目检测不同,这里是如果是 ssr(服务端渲染),立即更新(setTimeout 0),如果有
requestAnimationFrame
使用他更新,没有的话setTimeout 16
angular zone
区域 - connectedCallback 执行后,代码均执行在内部
- 设置输入值时
setInputValue
包括外部的
setAttribute
,因为attributeChangedCallback
;在connectedCallback
执行前的el.xxx
,都会设置输入值从而触发
兼容 angularjs 时使用
- 抱歉,没看过 angularjs 所以这块逻辑不懂
变更
- 逻辑视图保留了上一次的值,进行计算对比,如果不同,那么进行变更(也就是操作 dom 更新)
- 如果是管道类型的,只要传参不变,那么就不会再次执行
待办
- 变更检测部分由于时间部分没有录视频,如果哪里有问题还是不理解请提issues等我有时间处理https://github.com/wszgrcy/wszgrcy-blog/issues