前段时间遇到这样一个 bug,通过一个 click
事件跳转到一个新页面,新页面迟迟不加载;
经过多次测试发现,将鼠标移入某个 tab
,页面就加载出来了。
举个例子,页面内容无法加载,但是将鼠标移入下图的 消息 或者 历史 tab
,页面就加载出来了。
正常情况下,Angular
会在 组件初始化完毕后 自动进行变更检测,并更新视图。
但有时候,你可能希望在 组件实例化后 立即执行一次变更检测,以确保视图能够及时更新。这种情况下,我们可以在构造函数之后的代码中显式调用 detectChanges()
方法。
为了在切换路由后立即进行 变更检测 并渲染新页面,在路由导航结束后,Angular
提供了一个 Router.events
事件流来监听路由导航的状态。开发者可以订阅 NavigationEnd
事件,并在回调函数中调用 detectChanges()
方法。
import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
@Component({
// 组件元数据
})
export class MyComponent implements OnInit {
constructor(
private router: Router,
private cdr: ChangeDetectorRef,
) {}
ngOnInit() {
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
this.cdr.detectChanges(); // 在路由切换完成后调用 detectChanges
}
});
}
}
如此,就实现了跳转页面视图不展示的问题。
这里,简单介绍一下 Angular
的变更检测策略,包括 默认策略、OnPush 策略 和 无策略。
要关闭变更检测,可以将变更检测策略设置为 无策略 (ChangeDetectionStrategy.OnPush
)或 手动调用 detach()
方法来分离变更检测器。
1 变更检测策略
1.1 手动分离变更检测器
使用 detach()
方法来分离变更检测器,这样组件就不再与变更检测关联。
import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.component.html'
})
export class MyComponent implements OnInit {
constructor(private cdr: ChangeDetectorRef) {}
ngOnInit() {
this.cdr.detach(); // 分离变更检测器
}
}
在此方法中,我们需要将 ChangeDetectorRef
注入到组件中,并在 ngOnInit
生命周期钩子函数中调用 detach()
方法。这样就会分离变更检测器,从而关闭变更检测。
1.2 使用变更检测策略
将组件的变更检测策略设置为 ChangeDetectionStrategy.OnPush
,这会使得组件仅在输入属性发生变化时才进行变更检测。我们可以在组件的元数据中指定变更检测策略。
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.component.html',
changeDetection: ChangeDetectionStrategy.OnPush // 设置变更检测策略为 OnPush
})
export class MyComponent {}
使用此策略时,我们需要手动触发变更检测,例如通过注入 ChangeDetectorRef
并调用 detectChanges()
方法。
需要注意的是,关闭变更检测可能导致视图无法及时更新,因此应仔细考虑是否真正需要关闭变更检测。一般情况下,使用默认的变更检测策略并让 Angular
自动执行变更检测是推荐的做法。只有在特定的性能优化需求下才应该考虑手动关闭变更检测。
2 ChangeDetectionStrategy.OnPush
策略
这里,我们要注意,将组件的变更检测策略设置为 ChangeDetectionStrategy.OnPush
,这会使得组件仅在 输入属性 发生变化时才进行变更检测。输入属性指的是通过 @Input
装饰器定义在组件上的属性。这些属性用于从父组件向子组件传递数据。
当将组件的变更检测策略设置为 ChangeDetectionStrategy.OnPush
时,组件只会在 输入属性 发生变化时才触发变更检测和重新渲染。
举个例子,MyComponent
的组件,定义了一个输入属性 data
,只有每次 data
的值发生变化, Angular
才会自动触发变更检测并更新组件的视图。
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-my-component',
template: `
<div>{{ data }}</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush // 设置变更检测策略为 OnPush
})
export class MyComponent {
@Input() data: string;
}
通过将组件的变更检测策略设置为 ChangeDetectionStrategy.OnPush
,我们可以确保只有父组件修改了子组件的输入属性或者父组件手动调用 markForCheck()
方法,子组件才会执行变更检测、重新渲染,如此可以提高性能并减少不必要的变更检测。
事实上,除了输入属性发生变化,组件自身的事件(例如按钮点击、定时器等),也可以触发变更检测;
注意,当只有子组件内部的变量值发生改变而没有其他触发条件满足时,ChangeDetectionStrategy.OnPush
策略下的组件不会自动触发变更检测。这意味着组件的视图不会被更新。
2.1 子组件 ngModule 值变化
这里,可能会有人提问,子组件的 ngModule
值变化,会不会触发子组件的变更检测和重新渲染。
答案是不会,ngModule
是 Angular
中用于定义模块的装饰器函数,它通常在应用程序的根模块中使用,并且在启动时就确定了,它的变化不会被视为触发变更检测的条件。
子组件input
框的 ngModule
值发生变化时,并不能直接触发子组件的变更检测,那么 input
框中内容会发生变化吗?
当 input
框的 ngModule
值发生变化时,输入框中的内容仍然会根据新的值进行更新,因为这是由 ngModel
实现的。这种更新是通过 DOM 事件(如 input
或 change
)来触发的,而不是通过 Angular
的变更检测系统。
2.2 子组件 内部数据发生变化
这里,举一个内部数据发生变化的例子。
- 使用
ChangeDetectionStrategy.OnPush
策略;
count
的值定时发生变化,但是页面并未更新;
import { ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'test',
templateUrl: './test.component.html',
styleUrls: ['./test.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TestComponent implements OnInit, OnDestroy {
constructor(
private router: Router,
private cdr: ChangeDetectorRef,
) { }
count = 1;
ngOnInit(): void {
setInterval(() => {
this.count++;
}, 1000);
}
}
- 不使用
ChangeDetectionStrategy.OnPush
策略;
页面数据会刷新
2.3 父组件传值发生变化
父组件传值,直接修改了可变对象(如:数组、对象等),而没有创建一个新的引用,那么组件可能无法检测到这个改变,从而不会触发变更检测。
items: string[] = ['Item 1', 'Item 2', 'Item 3'];
this.items.push('New Item');
3 关闭变更检测
在 Angular
中,我们可以通过使用 ChangeDetectorRef
来关闭或禁用变更检测。ChangeDetectorRef
是一个服务,提供了与变更检测相关的方法。以下是一种关闭变更检测的方法:
注入 ChangeDetectorRef
服务:
import { Component, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-example',
template: `
<!-- Your component's template -->
`
})
export class ExampleComponent {
constructor(private cdr: ChangeDetectorRef) {}
}
在需要关闭变更检测的地方调用 detach() 方法
this.cdr.detach();
调用 detach()
方法会将组件从变更检测树中分离,意味着该组件及其子组件将不再进行自动的变更检测和视图更新。这可以帮助减少不必要的计算和渲染,提高性能。
需要注意的是,一旦你将组件从变更检测树中分离,它将不再自动响应输入属性的变化或事件的触发。如果你需要手动控制变更检测,可以使用 markForCheck()
方法来通知 Angular
运行变更检测。例如:
this.cdr.markForCheck();
使用 markForCheck()
方法可以标记组件及其父组件,使其在下一次变更检测周期中进行变更检测。
总结起来,通过 ChangeDetectorRef
的 detach()
方法,我们可以关闭变更检测以提高性能,并使用 markForCheck()
方法手动触发变更检测。请注意,在大多数情况下,不需要手动关闭变更检测,Angular
的默认行为通常已经足够高效和准确。
this.cdr.markForCheck()
和this.cdr.detectChanges()
是Angular中的变更检测相关方法,它们有一些区别和适用场景。
this.cdr.markForCheck()
: 调用markForCheck()
方法会通知Angular
在下一次变更检测周期中检查组件及其子组件,并执行相应的变更检测操作。该方法将标记组件为“已脏”(dirty),意味着组件可能发生了变化,需要进行检查。这对于在组件中进行异步操作,但可能没有直接触发变更的情况非常有用。调用markForCheck()
方法本身不会立即触发变更检测,而是等待下一次变更检测周期执行。
this.cdr.detectChanges()
: 调用detectChanges()
方法会立即触发一次变更检测,无论组件是否已被标记为“脏”。这将从组件树的根节点开始进行变更检测,并检查任何已标记为“脏”的组件。这可以用来强制立即进行变更检测,而不必等待下一次自动变更检测周期。但是,过度使用detectChanges()
方法可能会导致性能问题,因为它执行了全面的变更检测,而不只是对标记为“脏”的组件进行检查。
从使用角度来看,通常情况下应该优先使用this.cdr.markForCheck()
方法,因为它更高效且更符合 Angular
的变更检测机制。只有在特殊情况下,如在组件中调用了异步操作但未触发变更时,才应该使用this.cdr.detectChanges()
方法来强制立即进行变更检测。