变更检测

代码位置

  • 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

  • 使用Zonerun,runGuarded,runTask也会触发自动检测

    具体解释在下面

什么是微任务为空?

  • packages\core\src\zone\ng_zone.ts:395
  • 宏观的讲,就是某一时间段内异步任务全部执行完成后,微任务为空
  • 完整的讲,1. 某一时间段内没有异步任务,2. 某一时间段内没有使用 Zone.run,Zone.runTask,Zone.runGuarded

    这里的run,runGuarded,runTask包括NgZoneZone两种使用方法,只要调用,在执行完之前,微任务不为空

  • 微任务为空其实并不准确,但是函数命名是这么命名的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
  1. 对于不使用的组件,手动进行视图分离,即逻辑和视图不再关联,这样检测的时候也不会处理这个组件.
  2. 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 更新)
  • 如果是管道类型的,只要传参不变,那么就不会再次执行

待办