Angular视图更新的原理和方式

54 阅读5分钟

前言

我在开发的时候,总是习惯通过push去对表格数据进行操作(现在我觉得这是一个坏习惯)

例如新增数据,就直接给表格数组push一条同结构的空数据,截取一段我老项目中的代码:

// 面料表格添加一行数据
addData(type): void {
    // 新增一行数据,并设置好索引
    this.listOfData.push({
        position: this.listOfData.length,
        id0: '',
        id15: '',
        id16: '',
        id1: '',
        id2: '',
        id3: '',
        id4: '',
        id5: '',
        imgData: '',
        id6: '',
        id8: '',
        id9: '',
        id10: '',
        id11: '',
        id12: '',
        id13: ''
	});
}

这字段是我们公司的后端给的,命名问题请不要谴责我,我的代码洁癖很严重,都是被迫为了工作……

按理来说,我push了新数据之后,页面上的表格(或者说视图)就会(理所应当)的刷新加载对吧?我表格的数据都已经产生变化了。

然而并不会,为什么呢?

这就关系到Angular数据绑定的原理了,所以我写下了这篇文章来浅谈原理并列出几种可以触发Angular数据变更检测的方式

Angular的脏值检测

Angular的数据绑定是通过使用脏值检测(Dirty Checking)来监听数据的变化,决定是否更新视图,但并非所有(我们认为的)数据改变操作都会触发Angular的变更检测。

直接修改数组的内容是不会触发变更检测的,例如push方法。

为什么 push 不会刷新视图?

在 JavaScript 中,push 操作改变了数组的内容,但数组引用保持不变。例如:

const arr = [1, 2, 3];
arr.push(4); // 引用仍指向同一个数组

Angular 的变更检测会判断引用是否改变。如果数组引用未改变,Angular 将认为无需更新视图。

如何触发视图更新?

触发变更通常是创建新数组更改对象引用这两种方式。

解构运算符

**解构运算符(Spread Operator)**在 JavaScript 中是常用的语法糖,可高效地复制对象和数组,写法很像省略号:...

我们可以通过解构运算符来创建一个新的数组,并改变数组的引用,来触发变更检测:

const arr = [1, 2, 3];
const newArr = [...arr, 4];
console.log(newArr); // [1, 2, 3, 4]

同时我们还可以对对象进行解构:

const obj = { a: 1, b: 2 };
const newObj = { ...obj, c: 3 };
console.log(newObj); // { a: 1, b: 2, c: 3 }

放在实际的使用场景中,当我想为我的表格数据新增一条空数据时,我可以这么写:

addMlz(): void {
  this.materialsData.mlz_text = [...this.materialsData.mlz_text, {
    id1: '',
    id2: '',
    id3: '',
  }];
}

深拷贝和浅拷贝

除了解构运算符外,还有深拷贝和浅拷贝的方式来触发变更检测。

什么是浅拷贝?

浅拷贝仅复制对象的第一层属性,如果属性是引用类型,仅复制引用地址。

typescriptCopy codeconst obj = { a: 1, b: { c: 2 } };
const shallowCopy = { ...obj };
shallowCopy.b.c = 3;
console.log(obj.b.c); // 输出 3

什么是深拷贝?

深拷贝会复制整个对象的所有层级,修改副本不会影响原始对象。

typescriptCopy codeconst obj = { a: 1, b: { c: 2 } };
const deepCopy = JSON.parse(JSON.stringify(obj));
deepCopy.b.c = 3;
console.log(obj.b.c); // 输出 2

在本例中,如果直接 push 原始对象 i,会导致复制的行与原始行共享相同的引用。修改复制行的数据会同时影响原始行:

typescriptCopy codecopyMlz(i: any): void {
  this.materialsData.mlz_text.push(i); // 浅拷贝,共享引用
}

改进为深拷贝后:

typescriptCopy codecopyMlz(i: any): void {
  this.materialsData.mlz_text.push({ ...i }); // 深拷贝,创建独立副本
}

事件绑定

Angular 会自动检测模板中绑定的事件(如 clickinput)并触发变更检测。

<button (click)="onClick()">Click me</button>
onClick(): void {
	this.count += 1; // 自动触发变更检测
}

NgZone 服务

Angular 使用 NgZone 监控异步任务(如 setTimeoutPromise)。这些任务完成后,NgZone 会触发变更检测。

setTimeout(() => {
  this.data = 'Updated data';
}, 1000);

如果某些异步任务未触发变更检测,可以手动调用 NgZonerun 方法:

import { NgZone } from '@angular/core';

constructor(private ngZone: NgZone) {}

updateData(): void {
  setTimeout(() => {
    this.ngZone.run(() => {
      this.data = 'Updated data';
    });
  }, 1000);
}

ChangeDetectorRef

Angular 提供了 ChangeDetectorRef 服务,允许开发者手动控制变更检测的触发,对于某些不提供手动刷新接口的组件非常好用。

使用 detectChanges

显式触发当前组件的变更检测:

import { ChangeDetectorRef } from '@angular/core';

constructor(private cdr: ChangeDetectorRef) {}

updateData(): void {
  this.data = 'Updated data';
  this.cdr.detectChanges(); // 手动触发检测
}

使用 markForCheck

OnPush 策略下,标记组件以在下一次变更检测时重新检查。

this.data = 'Updated data';
this.cdr.markForCheck();

ApplicationRef.tick

ApplicationRef.tick 触发整个应用的变更检测,但这个操作过于……怎么说?昂贵?,通常仅在调试或特殊场景下使用,一般完全不建议。

import { ApplicationRef } from '@angular/core';

constructor(private appRef: ApplicationRef) {}

forceDetectChanges(): void {
  this.appRef.tick(); // 触发整个应用的检测
}

使用 async 管道

在模板中使用 async 管道处理 ObservablePromise 数据流时,async 会自动触发变更检测。

data$: Observable<string>;

ngOnInit(): void {
  this.data$ = of('Async data').pipe(delay(1000));
}
<p>{{ data$ | async }}</p>

手动调用 zone.run

如果代码运行在 NgZone 外部(例如通过第三方库执行异步操作),可以手动将代码回到 NgZone 内触发变更检测:

import { NgZone } from '@angular/core';

constructor(private ngZone: NgZone) {}

updateDataOutsideZone(): void {
  this.ngZone.run(() => {
    this.data = 'Updated data from outside NgZone';
  });
}

通过 ViewRef 或 TemplateRef 更新视图

可以通过动态组件或模板引用的 ViewRef 手动触发更新:

@ViewChild('templateRef', { read: ViewContainerRef })
viewContainerRef!: ViewContainerRef;

addDynamicComponent(): void {
  const ref = this.viewContainerRef.createEmbeddedView(this.templateRef);
  ref.detectChanges();
}

Observable订阅触发

通过 Observable 数据流,subscribe 后更新组件数据时会触发变更检测:

this.dataService.getData().subscribe(data => {
  this.data = data; // 触发变更检测
});

结构指令 (*ngIf, *ngFor) 引起的模板变更

结构指令本质上会引起视图的创建或销毁,因此任何与之相关的数据变化都会触发变更检测:

<div *ngIf="isVisible">
  <p>Conditionally visible content</p>
</div>
typescriptCopy codetoggleVisibility(): void {
  this.isVisible = !this.isVisible; // 触发变更检测
}

总结

以上是我对于Angular视图更新的原理、方式的一些浅谈,实际上原理并没有很复杂,以上十几种方式也基本能满足日常中的大部分需求,但对于想要知道原理并试图更进一步掌握Angular的人来说,这篇文章还是很有必要的OvO