4

什么是变化检测

简单来说变化检测就是Angular用来检测视图与模型之间绑定的值是否发生了改变,当检测到模型中绑定的值发生改变时,则同步到视图上,反之,当检测到视图上绑定的值发生改变时,则回调对应的绑定函数。

变化检测发生的时间

总结起来, 主要有如下几种情况:

  • 用户输入操作,比如点击,提交等
  • 请求服务端数据(XHR)
  • 定时事件,比如setTimeoutsetInterval

Angular并不是捕捉对象的变动,它采用的是在适当的时机去检验对象的值是否被改动,这个时机就是这些异步事件的发生。
这个时机是由 Zone.js去掌控的,它获取到了整个应用的执行上下文,能够对相关的异步事件发生、完成或者异常等进行捕获,然后驱动 Angular 的变化监测机制执行。

Zone.js的作用

实际上Zone,js有一个叫猴子补丁的东西。在Zone.js运行时,就会为这些异步事件做一层代理包裹,也就是说Zone.js运行后,调用setTimeout、addEventListener等浏览器异步事件时,不再是调用原生的方法,而是被猴子补丁包装过后的代理方法。代理里setup了钩子函数, 通过这些钩子函数, 可以方便的进入异步任务执行的上下文.

//以下是Zone.js启动时执行逻辑的抽象代码片段
function zoneAwareAddEventListener() {...}
function zoneAwareRemoveEventListener() {...}
function zoneAwarePromise() {...}
function patchTimeout() {...}
window.prototype.addEventListener=zoneAwareAddEventListener;
window.prototype.removeEventListener=zoneAwareRemoveEventListener;
window.prototype.promise = zoneAwarePromise;
window.prototype.setTimeout = patchTimeout;

关于Zone.js的详细内容可以看这篇文章

变化检测的过程

Angular都是组件化的, 并且所有的组件会构成一个组件树, Angular的变化检测就是以组件进行的,,对于每一个组件,都有一个对应的changeDetector, 因此, changeDetector之间也是一个树状结构, 最后我们需要记住的一点是,每次变化监测都是从 Component 树根开始的

另外,Angular的数据流是自顶而下,从父组件到子组件单向流动。单向数据流向保证了高效、可预测的变化检测。尽管检查了父组件之后,子组件可能会改变父组件的数据使得父组件需要再次被检查,这是不被推荐的数据处理方式。在开发模式下,Angular会进行二次检查,如果出现上述情况,二次检查就会报错:Expression Changed After It Has Been Checked Error。而在生产环境中,脏检查只会执行一次。
关于Expression Changed After It Has Been Checked Error, 笔者曾经遇到过, 可以看看这个例子

主动控制变化检测

通过我们可以主动控制Angular 的变化检测

它有以下方法:

  • markForCheck():把根组件到该组件之间的这条路径标记起来,通知Angular在下次触发变化监测时必须检查这条路径上的组件。该方法可与下文的OnPush模式搭配使用,可以看这个例子
  • detach():从变化监测树中分离变化监测器,该组件的变化监测器将不再执行变化监测,除非再次手动执行reattach()方法。
  • reattach():把分离的变化监测器重新安装上,使得该组件及其子组件都能执行变化监测。
  • detectChanges():手动触发执行该组件到各个子组件的一次变化监测。

下面以使用detectChanges()

使用方法

// 在组件中注入
  constructor(private changeDetectorRef: ChangeDetectorRef) {
  }
  
  // 直接使用
  test() {
    this.changeDetectorRef.detectChanges()
  }

angular变化检测策略

angular 提供了两种变更检测策略,除了上述得Default外还有一种OnPush 的检测机制

OnPush 与 Default 之间的差别:当检测到与子组件输入绑定的值没有发生改变时,变化检测就不会深入到子组件中去

一个OnPush的例子

app.comonent.ts

@Component({
  selector: 'app-root',
  template: `
    <h1>{{title}}</h1>
    <h2>user.name: {{user.name}}</h2>
    <button type="button" (click)="changeUserName()"> 改变属性
    </button>
    <button type="button" (click)="changeUserObject()">
      改变对象
    </button>
    <app-test [user]="user"></app-test>
  `,
})
export class AppComponent {
  title = 'OnPush Demo';
  user: User = new User({name: 'yunzhi'});

  changeUserName() {
    this.user.name = 'new name';
  }

  changeUserObject() {
    this.user = new User({name: 'new user'});
  }
}

test.component.ts

@Component({
  selector: 'app-test',
  template: `
    <div>
      <h3>test 组件</h3>
      <p>
        <label>User:</label>
        <span>{{user.name}}</span>
      </p>
    </div>`,
  // 使用OnPush模式只需要加上下面这段代码
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TestComponent implements OnInit {
  @Input() user: User;

  constructor() {
  }

  ngOnInit() {
  }

}

这时当我们点击改变属性按钮时test组件显示的并不会变化,只有改变user得引用test组件显示的才会变化,如下图所示
2020年05月09日-屏幕视频-14时55分15秒.gif

总结

Angular通过优秀的变化检测机制,让我们能够在数据发生了改变时准确的最小范围的修改DOM,同时,Angular还提供了两种检测策略,和在一个组件中控制变化检测的方法,灵活运用这些方法可以让我们的应用更加高效,虽然现在的项目还没有到需要这种效率。

参考文章

Angular开发实践(五):深入解析变化监测
Angular系列之变化检测(Change Detection)


笙歌会停
1k 声望45 粉丝

代码成就万世基积沙镇海 梦想永在凌云意意气风发