前言
我在开发的时候,总是习惯通过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 会自动检测模板中绑定的事件(如 click
、input
)并触发变更检测。
<button (click)="onClick()">Click me</button>
onClick(): void {
this.count += 1; // 自动触发变更检测
}
NgZone 服务
Angular 使用 NgZone
监控异步任务(如 setTimeout
、Promise
)。这些任务完成后,NgZone
会触发变更检测。
setTimeout(() => {
this.data = 'Updated data';
}, 1000);
如果某些异步任务未触发变更检测,可以手动调用 NgZone
的 run
方法:
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
管道处理 Observable
或 Promise
数据流时,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