当 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 触发。这种方式更加灵活,可以通过
RxJS
的debounceTime(), throttleTime()
去抖动,避免UI频繁地刷新。 - detectChanges 手动触发。通过程序代码而不是 dom event 改变 model 后,要手动调用 detectChanges() 更新该组件和子组件的 view
- detach 和 reattach
参考
- zone.js 包装过的异步事件列表
- Angular 2 脏检查过程
- Angular变化检测机制:改善的脏检查
- 双向绑定的简单实现 - 基于 “脏检测”
- 数据动态绑定的简单实现——基于ES5对象的getter/setter机制
- 通过去抖动和节流控制变化检测的频率