4

前言

本来实现了一个功能,却由于angular的自动检测变更,以为没有成功实现,遂放弃。现在我们来总结一下。

问题

写一个二维码实现功能,跑单元测试没有显示出来,
image.png
以为是不能通过base64设置img的src属性显示图片,后来放弃了。
经过老师的指点,原来是由于angular的自动检测变更并没有执行,虽然赋值了,但是并没有进行渲染,所以看起来并没有实现二维码功能。

后来跑了一下集成测试,竟然可以显示出来。猜想可能是因为集成测试其他请求使得重新渲染时顺带也渲染了二维码。但是请求返回有块又慢,这并不能保证万无一失,所以应该去手动调用重新渲染。

constructor(private changeDetectorRef: ChangeDetectorRef) {
}
  
/**
* 生成并显示二维码
* @param object 问卷
*/
generateAndShowQRCode(object: Questionnaire): void {
object.getQRBase64UrlCode()
  .subscribe(base64Data => {
    this.QRCode = base64Data;
    this.changeDetectorRef.detectChanges();
  });
}

或者

constructor(private componentRef: ComponentRef<ViewComponent>) {
}
/**
* 生成并显示二维码
* @param object 问卷
*/
generateAndShowQRCode(object: Questionnaire): void {
object.getQRBase64UrlCode()
  .subscribe(base64Data => {
    this.QRCode = base64Data;
    this.componentRef.changeDetectorRef.detectChanges();
  });
}

谁会触发变更检测

1.事件,当鼠标点击或者移动时就属于一个事件
2.XHR,从服务器获取数据 。
4.Timers,例如setTimeout(), setInterval() 这些异步方法。

谁在进行变更检测

angular有一个叫做zone的库,zone.js可以感知上述方法中组件的属性是否发生变化,从而决定是否重新渲染。

根据这篇博客
,我们可以得知DOM的更新依赖于 tick() 的触发,zone.js 帮助开发者省去了手动触发的操作。
有一个东西叫做ApplicationRef,它监听NgZonesonTurnDone事件。只要这个事件发生了,它就执行tick()函数,这个函数执行变更检测。
image.png
angular源码

image.png

说是监听,其实称为观察更为准确,监听是A每隔一段时间去看看B是否发生变化,观察是当B变化时告诉A,这样可以节省很多资源。

变更检测

image.png
整个angular应用可以看成是一个组件树,而每个组件都有自己的变更检测器(change detector),那么我们可以想象angular也包含着一科变更检测器树,当某个组件变更时,并不会只使得自己重新渲染,也不会使得整个整个应用重新渲染。对于子组件的引用总会伴随着对父组件属性的输入,当属性值改变,只有父组件重新渲染,而子组件无动于衷,这显然是不合理的。
默认情况下父组件变更检测,传入父组件对象的子组件也会发生变更加测,下面会说明原因。
image.png

更聪明的变更检测

变更所有的组件貌似也不太理想。因为一个父组件的一个属性改变,并不会导致他底下的所有子组件的@Input都发生改变,也就是说,并不是所有的子组件都需要重新渲染,如果我们能仅仅使得那些需要重新渲染的子组件重新渲染,岂不是更完美。
我们可以做到,只需要通过这两种数据结构,不可变(Immutables)数据结构和可观察(Observables)的数据结构,。

理解易变性 ( Mutability )

为了理解不可变的数据结构是什么,我们先来理解易变性是什么
假设我们有下面的组件

@Component({
  template: '<v-card [user]="user"></v-card>'
})
class VCardApp {

  constructor() {
    this.user = {
      name: 'zhang san',
      email: 'zhangsan@mengyunzhi.com'
    }
  }

  changeUser() {
    this.user.name = 'li si';
  }
}

这里最重要的部分是changeUser()方法改变了username属性,尽管这改变了,但是对user的引用是没有变的。

假设一些事件导致了changeUser()执行,变更检测会怎么执行呢?首先,user.name会被改变,然后它的子组件的变更检测器开始检测传过来的user是否发生改变,答案是没有改变,因为这个对象的引用没有被改变。然而,它的name属性被改变了。所以即便如此angular仍会为user执行变更检测。
这是由于javaScript中对象默认是易变的(multable)(除了基本数据类型),每当事件发生的时候angular会保守地对每个组件都跑一次变更检测。
这时候,不可变数据结构就可以派上用场了。

不可变对象 (Immutable Objects)

不可变对象保证了这个对象是不能改变的。这意味着如果我们使用着不可变对象,同时试图改变这个对象,那我们总是会得到一个新的引用,因为原来那个对象是不可变的。

var vData = someAPIForImmutables.create({
              name: 'Pascal Precht'
            });

var vData2 = vData.set('name', 'Christoph Burgdorf');

vData === vData2 // false

如上所示,我们不能轻易地改变name属性。如果发生改变,我们将得到一个新的对象。

减少检测的次数

当输入属性没有发生改变的时候,Angular 可以跳过对整个子树的变更检测。我们刚刚说了,"改变"意味着 "新的引用"。如果我们在 Angular 程序中使用不可变对象,我们只需要做的就是告诉 Angular,如果输入没有发生改变,这个组件就可以跳过变更检测。
比如说这个组件

@Component({
  template: `
    <h2>{{vData.name}}</h2>~~~~
    <span>{{vData.email}}</span>
  `,
})
class VCardCmp {
  @Input() vData;
}

当input值没有改变时我们可以跳过对子组件的监测变更。只需要设置变更检测策略为onPush就可以了

@Component({
  template: `
    <h2>{{vData.name}}</h2>~~~~
    <span>{{vData.email}}</span>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
class VCardCmp {
  @Input() vData;
}

image.png

我来大致梳理一下。首先,angular会对子组件进行监测变更,即使@input对象并没有改变,对象的属性也没有改变。我们希望对象属性不改变时不执行对子组件的监测变更。我们使用不可变对象来来确保对象属性改变时引用也改变,从而对子组件执行检测变更。再使用changeDetection: ChangeDetectionStrategy.OnPush来使得对象属性不改变时不进行对子组件的监测变更。

observables

题主也没有看明白相关资料,未完待续。。。

具体问题具体分析

遇到我们不确定的方法,不知道他是否会触发变更检测,这时候就需要单元测试帮助我们具体试一试,实践是检验真理的唯一标准。
而当我们继承测试时,这种问题由于其他的影响,我们很小概率发现。这种很小概率发生的bug,就算发现了也很难还原错误,这就导致很难找出原因。而单元测试就可以轻而易举的发现问题。


小强Zzz
1.2k 声望32 粉丝

下一篇 »
深入aop