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语法内部实现与这个差不多.


事件绑定:

事件绑定原理很简单:

  1. 用户操作触发DOM事件通知
  2. Angular监听到了通知,然后执行模板语法,这里就是将input控件的输入赋值给了currentHero.name.

数据绑定:

数据绑定就没那么简单了.

你以为是这样?


很遗憾,JS语言并没有属性变化发通知的机制,所以angular也不知道谁变了?什么时候变了?

那怎么办?

angular的变化检测机制是:

  • 谁变了?

全查一遍,新旧值一对比,知道了!(树深度遍历,脏值检查.)

  • 什么时候变了?

可能引起数据变化的事件后,我都查!网络请求,DOM树通知,setTimeout,setInterval等异步事件.

  • 看起来会查很多遍?

没错,反正比你想的要多得多.

  • 这么干会不会有性能问题?

angular保证几毫秒就能检查成百上千的简单检查,比你想象的要快!

挺笨地一个办法!

但是,由于变化检测永远自上往下,不会反方向,不会级联.简化了逻辑,实现,出错的概率降低了,debug也简单了. 牺牲性能换取了简化.

这里的重点是:自上而下!


再看刚才的input的例子:

  1. 代码修改了currentHero.name的值
  2. 触发整个组件树的变化检查
  3. input显示了改后的值


当然,Angular也不是傻傻的每次全部检测,这只是默认行为,大家还是可以优化的

两个思路:

  1. 我知道我没变,别查我!
  2. 我变了,只查我!

这里的我包含自己以及子树


OnPush策略

组件上声明这个

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
})

ok,完事.这样Angular就是只在你变的时候查你.

注意,不查你也就意味着同样不查你的儿子,儿子的儿子,...,...


OnPush策略具体是怎么个流程?

  1. 第一次迎接检查,这样才能保证初始化正确嘛.
  2. 然后就关门上锁,怒拒检查.
  3. 当变化时,开门喜迎检查.
  4. 检查完了?关门上锁.下次还拒.

这么牛掰呀,不变不查,变了才查!那对组件要求也挺高吧?

木错!组件的显示,完全依赖注入属性,木有内部属性影响显示.才能申请这个资质.

注入给你的属性毕竟都是父组件给你的,父组件检查一下这几个值,就知道你变没变,是否需要查你了.

@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是不会察觉出来的.

所以这里对注入属性的类型是有要求的:

  1. 基本数据类型
  2. 不可变对象
  3. 可观测对象.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(); 
}
  1. 如果你的组件显示变化你完全控制,那么你完全可以用这个方法.
  2. 如果你的组件变化太过频繁,也可以用这个方法做一个去抖动,如1s内最多刷新一次.
  3. 如果你的组件需要一个定时刷新.


这里用到了 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) {
        if (currView.def.flags & ViewFlags.OnPush) {
            currView.state |= ViewState.ChecksEnabled;
        }
        currView = currView.viewContainerParent || currView.parent;
   }
}


列表优化:

如果说你的列表数据变化完全自己控制,那拒绝检测这个策略你用得上.


疑问:

  • 明明已经申请了OnPush,input属性没有修改,为什么组件的ngDoCheck() ,ngAfterContentChecked() ,ngAfterViewChecked() 这几个方法还是调用了呢?

因为这几个方法是组建的父组件调用的,这只能说明变化检测进入了父组件.


  • 没有申请Onpush,输入Hero对象的内部属性变了,ngOnChanges() 没有被调用,为什么绑定依然显示正确呢?

ngOnChanges()方法被调用决定于父组件变化检测,当父组件变化检测时,会挨个检查子组件的input值,如果变了,就会运行这个方法.如上面所说,input对象内部属性变化,angular检测不出来,所以没有调用这个方法.

因为没有申请OnPush,所以变化检测依然会进入组件.Angular会检测组件所有的模板绑定,新旧值对比,如果变化了,就刷新DOM.

这两者是在不同的过程中,没有啥关系.


  • 组件申请了OnPush,调用reattach()没有效果?

其实也不是没效果,就是太短了,当组件变化检测完后,一看自己有onPush,就关了ChecksEnabled,相当于自动detach()了.


参考:

vsavkin.com/change-dete

vsavkin.com/immutabilit

blog.angularindepth.com

blog.angularindepth.com

发布于 2017-09-20 20:35