0%

angular-change-detection

Change Detection是如何触发的?

前端框架的主要任务就是根据数据的变化来更新UI,Angular也不例外,而变更检测(Change Detection)就是完成这一任务的核心机制。

change-detection-flow

React或者Vue这类框架中,触发UI更新的操作是由开发者手动完成的,比如调用setState或者$set
React中使用setState来触发变更检测。

1
this.setState({ count: this.state.count + 1 });

Vue中使用$set来触发变更检测。

1
this.$set(this.data, 'count', this.data.count + 1);

而在Angular中,变更检测是自动触发的,那么Angular是如何实现自动触发变更检测的呢?要解释清楚这个问题,大家首先要明白javascript这门语言的特殊性,JS的行为在运行时是可以被改写的,也就是说可以做Monkey Patch,而Angular正是利用了这一特性来实现自动变更检测的。

在Angular app启动时,Angular框架会对一些底层的异步事件进行Monkey Patch(在这些事件中加入额外的逻辑),这样就可以在异步事件执行时自动触发变更检测。Angular使用Zone.js来完成这一过程。

比如下面这个addEventListener的例子,Angular会在addEventListener的回调函数执行完后,自动触发变更检测。这就是Monkey Patch的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
function addEventListener(eventName, callback) {
// call the real addEventListener
callRealAddEventListener(eventName, function() {
// first call the original callback
callback(...);
// and then run Angular-specific functionality
var changed = angular.runChangeDetection();
if (changed) {
angular.reRenderUIPart();
}
});
}

在Angular程序启动时,会创建一个Zone,很多人刚开始对Zone比较迷惑,其实它很简单,就是一个执行上下文(Execution context)这个Zone会监控所有的异步操作,当异步操作执行时,Angular会运行变更检测。Zone.js会对浏览器中的所有异步操作进行monkey patch,这样Angular就能知道何时运行变更检测。

这些被monkey patch的异步操作包括

  • setTimeout, setInterval,
  • addEventListener,
  • XMLHttpRequest,
  • fetch,
  • Promise,
  • MutationObserver
  • requestAnimationFrame
  • async/await - 这个会降级到Promise,因为Zone.js无法直接监控async/await,所以会降级到Promise。

这种方式大大降低了开发人员写代码时的心智负担,但是这种方法也有一些弊端,那就是如果某些异步操作无法被Zone.js支持的话,那么Angular就无法对这些异步操作进行变更检测。比如IndexedDB的一些回调函数。

Change Detection是如何工作的?

每一个Angular Component都有一个与之关联的change detector, 这个change detector是在Angular App启动时创建的。当变更检测触发时,Angular会对Template中的每一个Expression进行检查,这个Expression一定是包含着Component中的某个或者某些变量,当该变量的当前值和之前的值不一致时,Angular就会使用新值渲染Template,这样就完成了UI的更新。

当然,这个操作不是一次一次触发的,Angular会对所有变化了的值进行标记,比如isChanged = true,待标记完成后,一次性进行更新。Angular对新旧值进行比较时用的时looseNotIdentical()算法,也就是===,但是对于NaN有个特殊处理,也就是说NaN === NaN返回true(注意,在JS中NaN === NaN返回false)

Angular会对整个Component tree进行检测,并遵循如下原则:(不考虑OnPush的情况下)

  1. 从整个组件树的根结点进行检测。
  2. 组件树中每个结点都被检测。
  3. 检测的方向是自顶向下。
  4. 组件树的遍历算法是深度优先遍历(DFS)算法。

每个被检测到的component都会执行以下操作:

  1. 运行Life Cycle方法
  2. 更新绑定 - Updating bindings.
  3. 刷新UI

一个常见的误解是:Angular在变更检测时会重新渲染整个组件树,这是不对的。Angular足够智能,它只会更新那些发生了变化的部分,而不是整个组件树。

如何关闭某个组件的Change Detection?

很简单,只需要在组件的constructor中detach change detector即可。

1
2
3
4
5
export class MyComponent {
constructor(private cd: ChangeDetectorRef) {
cd.detach();
}
}

这是一个很偏门的需求,使用的场景很少,但是也存在,比如如果后端通过websocket发送了大量的数据,而前端不可能这儿快的实时处理这些数据,所以就折中一下,每隔5秒钟更新一下UI,那么就可以用此方法。

我们现调用detach()方法,这样就可以关闭这个组件的变更检测了,然后再使用setInterval来定时手动触发变更检测,更新UI。

1
2
3
4
5
6
7
8
export class MyComponent {
constructor(private cdf: ChangeDetectorRef) {
this.cdf.detach();
setInterval(() => {
this.cdf.detectChanges();
}, 5000);
}
}

Take the DOM event as an example, when user click a button on page, Angular will run change detection to update the view. Angular will run change detection after the event handler is executed.

Here is the pseudo code, you can find the real code in packages/zone.js/lib/browser/event-target.ts in angular source code.(Note, initially, zone.js is a independent library, but it is moved to angular’s mono-repo.)

1
2
3
4
5
6
7
8
9
10
11
// Store a reference to the original addEventListener
const nativeAddEventListener = window.EventTarget.prototype.addEventListener;

// Replace the original addEventListener with a patched version
window.EventTarget.prototype.addEventListener = function(eventName, handler, options) {
// Call the native addEventListener with the provided arguments
nativeAddEventListener.call(this, eventName, handler, options);

// Additional functionality to track the state of asynchronous operations
// This could involve interacting with Zone.js's internal data structures
};

Change Detection steps

  1. An event occurs (user action, timer, AJAX response, etc.).
  2. Angular’s zone (NgZone) notifies Angular that an async operation has completed.
  3. Angular triggers change detection starting from the root component.
  4. For each component, it checks the template bindings against the current component state.
  5. If a binding has changed, Angular updates the DOM accordingly.
  6. Components using OnPush strategy only check if their inputs have changed or if marked explicitly.
  7. The process continues down the component tree until all components are checked.

What does change detection do in Angular?

  1. Update all bindings in templates.
  2. Update the view.

Angular中,每个Component都会关联一个Change Detector, 这个Change Detector是在Angular App启动时创建的(是所有Component共用一个Change Detector还是每个Component有自己的Change Detector?)。

这个Change Detector的工作就是检查每个Component的Template中的Expression是否发生了变化,如果发生了变化,就会更新View。

NgZone

Angular uses NgZone to detect changes in the application. NgZone is a class that provides a way to run code in the Angular zone. It is used to detect changes in the application.

You can easily see NgZone by logging it to the console.

1
2
3
4
5
6
export class AppComponent {
constructor(zone: NgZone) {
console.log((zone as any)._inner.name); // angular
console.log((zone as any)._outer.name); // root
}
}

NgZone is instantiated during the bootstrap phase.

How to determine when change detection is triggered in a component?

注意:下面这个结论是错误的,留在这里时刻提醒自己,不要再犯这种错误。详情请看这里

You can use the ngDoCheck lifecycle hook to determine when change detection is triggered in a component. ngDoCheck is called every time change detection is triggered in a component.

1
2
3
4
5
export class AppComponent {
ngDoCheck() {
console.log('Change detection triggered');
}
}

ngOnChanges

ngOnChanges is a lifecycle hook that is called when a component’s input properties change. It is used to detect changes in the component’s input properties.

What’s the scenarios to use ngOnChanges?

  1. When you want to detect changes in the component’s input properties.
  2. When you want to avoid function call in template. see here

References

  1. https://angular.love/the-latest-in-angular-change-detection-zoneless-signals - 这个写得太好了,仔细读之。