Angular之变化检测

Posted by Shen Chaoran on July 7, 2018

当 VM 变更时,如何将其更新到 UI 上面,即如何实现数据绑定(单向数据流动)的? Angular 认为视图的更新是由异步事件的触发引起的,如鼠标交互事件、Ajax 请求、 timer等,那么只需要在异步事件执行完后检查组件视图是否需要更新即可。

几种变化检测的方法

数据劫持:getter setter

脏检查:zone.js

zone是一种拦截跟踪异步事件的机制。

Zone.current.fork({
    // 局部作用域变量
    anyAttr: '',
    // 钩子函数
    beforeTask: () => {},
    afterTask: () => {},
    onError: () => {},
    onScheduleTask: () => {},
    onInvokeTask: () => {}
}).run(function () {
    Zone.current.inTheZone = true;
  
    setTimeout(function () {
        console.log('in the zone: ' + !!Zone.current.inTheZone); 
    }, 0);
});

console.log('in the zone: ' + !!Zone.current.inTheZone);

Angular 2+ 使用的 zone.js 会重写所有异步事件,在数据模型发生改变时,通过zone的钩子通知视图做出改变。

发布订阅模式:Observable, Promise, addEventListener

ng 1.x 的实现


Angular 的变化检测器

变化检测树

Ng的每个组件都有一个变化检测器,因此,和组件类似,App整体的变化检测器也是一个树。 当树中的任何输入数据发生变化时,变化检测器都会从跟组件开始检测。每次变化检测过程中每个组件只会检测一遍。 这种默认的变化检测方法会执行一些冗余的树枝检测,所以Ng也提供了别的检测方式。

变化检测的监听 ngOnChanges

变化检测的监听只包括输入属性的变化,而且必须是通过父组件传入数据的变化引起的,在本组件内手动改变输入属性的值不会触发ngOnchanges钩子函数。

import { Component, Input, OnChanges, SimpleChange } from '@angular/core';

@Component({
    selector: 'exe-child',
    template: `
     <p></p>
    `
})
export class ChildComponent implements OnChanges{
    @Input() text: string;

    ngOnChanges(changes: {[propName: string]: SimpleChange}) {
        console.dir(changes['text']);
    }
}

其中,SimpleChange对象结构如下图

变化检测策略

Ng 有两种变化检测策略,Default 和 OnPush。

Default 策略

  • 检查整颗树?还是整个子树?
  • 每一次异步操作后都会触发

OnPush 策略

使用这种策略时,如果组件的输入属性没有发生变化,则变化检测树会跳过该子树。

import { Component, Input, ChangeDetectionStrategy } from '@angular/core';

@Component({
    selector: 'profile-card',
    template: `
       <div>
         <profile-name [name]='profile.name'></profile-name>
         <profile-age [age]='profile.age'></profile-age>
       </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProfileCardComponent {
    @Input() profile: { name: string; age: number };
}

需要注意的是,OnPush 需要和 Immutable 对象结合使用。由于 js 内存堆栈的设计,复杂对象的内容存在堆中,地址存在栈中。而变化检测是根据 === 来判断的,所以当对象的属性改变时,是不会触发变化检测的。 而使用 Immutable 对象,则不仅会改变对象内容,也会改变对象地址,相当于重新创建了一个对象,会触发变化检测器。

补充知识Immutable Data:在 js 中,默认的对象都是 mutable 的。Immutable 对象一般通过深拷贝结构共享来实现。 结构共享:

在程序中,我们应尽量使用 OnPush 策略。

其他常用的变化检测方式

ChangeDetectorRef

ChangeDetectorRef 是组件的变化检测器的引用,我们可以在组件中的通过依赖注入的方式来获取该对象: ChangeDetectorRef 的接口

export abstract class ChangeDetectorRef {
  // 在组件的 metadata 中如果设置了 changeDetection: ChangeDetectionStrategy.OnPush 条件,那么变化检测不会再次执行,除非手动调用该方法。
  abstract markForCheck(): void; 
  // 从变化检测树中分离变化检测器,该组件的变化检测器将不再执行变化检测,除非手动调用 reattach() 方法。
  abstract detach(): void;
  // 重新添加已分离的变化检测器,使得该组件及其子组件都能执行变化检测
  abstract reattach(): void;
  // 从该组件到各个子组件执行一次变化检测
  abstract detectChanges(): void;
}

Observables

使用 Observable 和 markForCheck 执行变化检测。

import { Component, Input, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Observable } from 'rxjs/Rx';

@Component({
    selector: 'exe-counter',
    template: `
      <p>当前值: </p>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent implements OnInit {
    counter: number = 0;

    @Input() addStream: Observable<any>;

    constructor(private cdRef: ChangeDetectorRef) { }

    ngOnInit() {
        this.addStream.subscribe(() => {
            this.counter++;
            this.cdRef.markForCheck();
        });
    }
}

单向数据流

在Angular中,单向数据流规则指数据模型发生变化,Angular变更检测,调用渲染器把应用的数据模型转化为DOM数据结构(视图模型)的过程中是单向的,不可发生其他改变的方向。 即Angular从组件树的顶部到底部的整个渲染扫描过程都是单向的。

单向数据流双向数据绑定没有联系,只是名字上容易让人联想到一起!

我们希望确保在将数据转换为视图的过程中,不会进一步修改数据。数据从组件类流向代表它们的DOM数据结构,生成这些DOM数据结构的行为本身不会对数据进行进一步修改。在Angular变更检测周期,任意会改变数据状态的行为都会抛出异常从而终止。

但在Angular的变更检测周期中,组件的生命周期钩子会被调用,这意味着我们编写的代码在该过程中被调用,该代码有可能引发数据状态发生改变。
可以使用setTimeout将数据修改延迟到下一个变更周期

单向数据流的作用:

  • 首先是因为它有助于从渲染过程中获得很好的性能。
  • 它确保了当我们的事件处理程序返回并且框架接管渲染结果时,没有什么不可预测的发生。
  • 防止数据vs视图不一致的错误。

总结

常用的变化检测方法有:

  • Immutable + OnPush:Input属性更新触发。这种方式简单,可以应对大部分情况,但是当Input的数据结构复杂时,可能会引起性能问题。
  • Observable + OnPush:在subscribe中 markForCheck 触发。这种方式更加灵活,可以通过 RxJSdebounceTime(), throttleTime() 去抖动,避免UI频繁地刷新。
  • detectChanges 手动触发。通过程序代码而不是 dom event 改变 model 后,要手动调用 detectChanges() 更新该组件和子组件的 view
  • detach 和 reattach

参考