angular2双向绑定与变化检测


很久很久以前,微软的公司出了一套桌面应用框架WPF,其中,有一个全新的模式:MVVM.而MVVM的核心机制,就是双向绑定.
什么是双向绑定?
这幅图诠释的很清晰, 框架维护了 页面(View),数据(Data)之间的一致性 ,解放了可怜的程序猿.
如今,MVVM已经是前端流行框架必不可少的一部分,web.android,ios也都有它.
双向绑定,也是angular2的核心概念之一,angular2的双向绑定是这样的:
data=>view : 数据绑定.模板语法 []
view=>data : 事件绑定,模板语法 ()
Angular其实并没有一个双向绑定的实现,它的 双向绑定就是数据绑定+事件绑定 .模板语法[()]
[]+()=[()].恩,没毛病.
Angular官方给的例子
<input [value]="currentHero.name" (input)="currentHero.name=$event.target.value" >
这是个input控件的双向绑定语法,很清楚的说明了双向绑定与两个单向绑定的关系.
这里没有用ngModule语法.ngModule语法内部实现与这个差不多.
事件绑定:
事件绑定原理很简单:
- 用户操作触发DOM事件通知
- Angular监听到了通知,然后执行模板语法,这里就是将input控件的输入赋值给了currentHero.name.
数据绑定:
数据绑定就没那么简单了.
你以为是这样?
很遗憾,JS语言并没有属性变化发通知的机制,所以angular也不知道 谁变了?什么时候变了?
那怎么办?
angular的变化检测机制是:
- 谁变了?
全查一遍,新旧值一对比,知道了!(树深度遍历,脏值检查.)
- 什么时候变了?
可能引起数据变化的事件后,我都查!网络请求,DOM树通知,setTimeout,setInterval等异步事件.
- 看起来会查很多遍?
没错,反正比你想的要多得多.
- 这么干会不会有性能问题?
angular保证几毫秒就能检查成百上千的简单检查,比你想象的要快!
挺笨地一个办法!
但是,由于变化检测永远自上往下,不会反方向,不会级联.简化了逻辑,实现,出错的概率降低了,debug也简单了. 牺牲性能换取了简化 .
这里的重点是: 自上而下 !
再看刚才的input的例子:
- 代码修改了currentHero.name的值
- 触发整个组件树的变化检查
- input显示了改后的值
当然,Angular也不是傻傻的每次全部检测,这只是默认行为,大家还是可以优化的
两个思路:
- 我知道我没变,别查我!
- 我变了,只查我!
这里的我包含自己以及子树
OnPush策略
组件上声明这个
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
ok,完事.这样Angular就是 只在你变的时候查你 .
注意,不查你也就意味着同样不查你的儿子,儿子的儿子,...,...
OnPush策略具体是怎么个流程?
- 第一次迎接检查,这样才能保证初始化正确嘛.
- 然后就关门上锁,怒拒检查.
- 当变化时,开门喜迎检查.
- 检查完了?关门上锁.下次还拒.
这么牛掰呀,不变不查,变了才查!那对组件要求也挺高吧?
木错!组件的显示, 完全依赖注入属性 , 木有内部属性影响显示. 才能申请这个资质.
注入给你的属性毕竟都是父组件给你的,父组件检查一下这几个值,就知道你变没变,是否需要查你了.
@Component({
moduleId: module.id,
selector: 'app-hero-card',
template: `
<p>{{Title}}</p>
<p>{{Hero.name}} is {{Hero.title}}<p>
changeDetection: ChangeDetectionStrategy.OnPush //在这里!
export class HeroCardComponent {
constructor(public cd: ChangeDetectorRef) {
@Input()
Hero: Hero;
@Input()
Title: string;
export class Hero{
name:string;
}
如上面的例子,依赖属性Title:string.父组件新旧值一比就知道变没变了.
如果注入的值是个对象,对象内部的属性变化了,Angular怎么知道?
如这个例子,如何知道Hero.name变了?
序列化比较?遍历Hero属性值新旧比较?
琢磨一下也觉得不容易做到,而且还必须快.
所以Angular这里 只是比较引用 .
上面的例子,代码修改了Hero.name,Angular是不会察觉出来的.
所以这里对注入属性的类型是有要求的:
- 基本数据类型
- 不可变对象
- 可观测对象.Obervable<object>
不可变对象的话,可以用一些库. Immutable.js
可观测对象的话,你需要保证对象变化时发通知,哪怕你发的还是之前那个对象.
@Component({
selector: 'app-hero-detail-observable',
template: `
<p>{{(HeroObservable | async)?.Name}}</p>
changeDetection: ChangeDetectionStrategy.OnPush,
export class HeroDetailObservableComponent {
@Input()
HeroObservable: Observable<Hero>;
OnPush策略是怎么做到的?
其实每个组件内部维护了一个属性 state,它是按位处理的flag标记.其中就有一位代表 ChecksEnabled.
view.state & ViewState.ChecksEnabled == true //变化检测允许进入
view.state & ViewState.ChecksEnabled == false//变化检测不允许进入
父组件变化检测时,会挨个检测子组件的状态(这时会调用子组件的 ngDoCheck() , ngOnChanges() 方法)
当子组件设置了onPush,父组件检测到子组件的input变化了,就会打开 它的ChecksEnabled.
if (view.def.flags & ViewFlags.OnPush) {
view.state |= ViewState.ChecksEnabled;
}
当子组件变化检测完成时,就会关闭它的ChecksEnabled
if (view.def.flags & ViewFlags.OnPush) {
view.state &= ~ViewState.ChecksEnabled;
}
手动控制刷新
当组件自身完全知道自己什么时候变了
可以拒绝常规的变化检测.当自己变化后,手动刷新自己.
constructor(public cd: ChangeDetectorRef) {
cd.detach(); //拒绝变化检测
onChange(){
//强刷一次自己及子树.这个方法忽略了自己的ChecksEnabled状态.
this.cd.detectChanges();
}
- 如果你的组件显示变化你完全控制,那么你完全可以用这个方法.
- 如果你的组件变化太过频繁,也可以用这个方法做一个去抖动,如1s内最多刷新一次.
- 如果你的组件需要一个定时刷新.
这里用到了 ChangeDetectorRef .这是一个更细致的让你控制检查的类:
它的一些重要方法:
detach(): void { //关门上锁
this._view.state &= ~ViewState.ChecksEnabled;
reattach(): void {//喜迎检查
this._view.state |= ViewState.ChecksEnabled;
}
之前说了,如果你祖宗怒拒检查,那你把门打的再开,也没用.所以,ChangeDetectorRef提供了这个方法
//从自己往上,把ChecksEnabled都打开.
markForCheck{
let currView: ViewData|null = view;
while (currView) {