0%

angular 变更检测详解

The basic task of change detection is to take the internal state of a program and make it somehow visible to the user interface.

变更检测的基本任务是获取程序内部的状态,并使该状态对用户界面可见。

This state can be any kind of objects, arrays, primitives[原语,基本类型],… just any kind of JavaScript data structures.

这种状态可以是任何类型的对象,数组,基本类型。。。等等任何一种JavaScript的数据结构。

This state might end up as paragraphs, forms, links or buttons in the user interface and specifically[特别的] on the web, it’s the Document Object Model(DOM).

这种状态可能会以用户界面中(特别是Web中的)段落、表单、链接或按钮结束,它就是文档对象模型(DOM)。

So basically we take data structures as input and generate DOM output to display it to the user. We call this process rendering.

因此,基本上,我们将数据结构作为输入并生成DOM输出以将其显示给用户。我们把这个过程叫做渲染。

However, it gets trickier when a change happens at runtime.

但是,在运行时发生变化会使它变得棘手。

Some time later when the DOM has already been rendered. How do we figure out[弄清楚] what has changed in our model, and where do we need to update the DOM?

一段时间后,DOM渲染完毕,我们如何弄清楚我们的模型中发生了什么?并且什么时候我们需要去更新DOM?

Accessing the DOM tree is always expensive, so not only do we need to find out where updates are needed, but we also want to keep that access as tiny as possible.

访问DOM树总是很昂贵的,因此,我们不仅需要找出需要更新的位置,而且还希望使访问频率尽量少。

This can be tackled[已解决] in many different ways.

这可以通过许多不同的方法解决。

One way, for instance, is simply making a http request and re-rending the whole page.

例如,一种方法就是简单的发出http请求,并重新呈现整个页面。

Another approach[方法] is the concept[概念] of diffing the DOM of the new state with the previous state and only render the difference, which is what ReactJS is doing with Virtual DOM.

另一种方法是将新状态的DOM与先前的DOM进行区分并仅重新渲染差异的DOM的概念,这就是ReactJS对Virtual DOM所做的工作。

So basically the goal of change detection is always projecting data and it’s change.

所以,基本上变更检测的目标始终是投影数据以及数据的变更。

What causes change? 是什么导致变化?

Now that we know what change detection is all about, we might wonder, when exactly can such a change happen? When does Angular know it has to update the view?

现在我们知道了变更检测的全部内容,我们可能胡想知道,什么时候才会发生这种变更?Angular怎么知道何时应该去更新视图?

Well, let’s take a look at the following code:
app.component.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Component } from '@angular/core';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.less'],
})
export class AppComponent {

firstname: string = 'Pascal';
lastname: string = 'Precht';

runtimes: number = 0;

changeName() {
this.firstname = 'Brad';
this.lastname = 'Green';
}
}

app.component.html:

1
2
<h1>{{firstname}} {{lastname}}</h1>
<button (click)="changeName()">change name</button>

The component above simple displays two properties and provides a method to change them when the button in the template is clicked.

上面这个组件简单的展示了两个属性并提供了一个方法,当模板中的按钮被点击时来改变属性的值。

This moment this particular button is clicked is the moment when application state has changeed, because it changes the properties of the component.

单击这个特定的按钮的时刻,是应用程序状态更改的时刻,因为它更改了组件的属性。

That’s the moment we want to update the view.
这个时刻是我们需要更新视图的时刻。

Here’s another one:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
selector: 'app-contacts-app',
template: `
<p>
contacts-app works!
</p>
`,
styles: []
})
export class ContactsAppComponent implements OnInit {

contacts: Contact[] = [];

constructor(
private httpClient: HttpClient,
) { }

ngOnInit() {
return this.httpClient.get('/assets/data/contacts.json').subscribe(contacts => this.contacts = contacts});
}

}

This components holds a list of contacts and when it initializes, it performs a http request.

该组件维护一个联系人列表,并在初始化后发出一个http请求。

Once this request comes back, the list gets updated. Again, at this point, our application state has changed so we will want to update the view.

一旦请求返回后,列表就会接收到变更,同样,在这一点上,我们的应用程序的状态也会收到变更,因此,我们将更新视图。

Basically application state change can be caused by three things:

  • Events - click, submit …
  • XHR - Fetching data from a remote server.
  • Timer - setTimeout(), setInterval()

基本上,应用的状态变更可能由三种情况导致变更:

  • 用户事件
  • 从远程服务器接收数据
  • 定时器

They are all asynchronous.

他们都是异步的。

Which bring us to the conclusion[结论] that, basically whenever some asynchronous operation has been performed[已执行], our application state might have changed.

我们可以得出一个结论,基本上,每当执行了一些异步操作时,我们的应用程序的状态就可能已经发生了更改。

This is when someone needs to tell Angular to update the view.

而这个时候,就是需要告诉Angular去更新视图了。

Who notifies Angular? 谁通知Angular?

Alright, we now know what causes application state change. But what is it that tells Angular, that at this particular moment, this view has to be updated?

好了,我们现在知道是什么导致Angular应用状态更改的原因了。但是,是什么告诉Angular需要再特定时刻更新视图?

Angular allow us to use native APIs directly[直接的]. There are no interceptor[拦截器] methods we have to call so Angular gets notified to update the DOM. Is that pure magic?

Angular允许我们直接调用原生的API。我们没有必须调用的拦截器方法通知Angular去更新DOM。难道是用魔法吗?

You know that Zones take care of this. In fact, Angular comes with it’s own zone called NgZone, which we’ve written about in our article Zones in Angular. You might want to read that, too.

你应该知道”Zones”做做这些工作,实际上,Angular封装了自己的“NgZone”,如果想了解更多,可以查看文章Zones in Angular

The short version is, that somewhere in Angular’s source code, there’s this thing called ApplicationRef, which listens to NgZones`onTurnDone` event.

简短的来说,在Angular的源码库中的某个地方,有个叫做ApplicationRef的东西,它监听NgZonesonTurnDone事件。

Whenever this event is fired, it executes a tick() function which essentially[实际上] performs change detection.

每当触发此事件时,它都会执行tick()函数,该函数实际上执行变更检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ApplicationRef {

changeDetectorRefs:ChangeDetectorRef[] = [];

constructor(private zone: NgZone) {
this.zone.onTurnDone
.subscribe(() => this.zone.run(() => this.tick());
}

tick() {
this.changeDetectorRefs
.forEach((ref) => ref.detectChanges());
}
}

Change Detection 变更检测

Okay cool, we now know when change detection is triggered, but how is it performed?

好的,我们现在知道何时执行变更检测,但是它是如何执行的?

Well, the first thing we need to notice is that, in Angular, each component has its own change detector.

好吧,我们需要注意的第一件事就是,在Angular里面,每个组件都有它自己的变更检测器。

This is significant[重要的] fact, since this allow us to control, for each component indevidually[分别的], how and when change detection is performed! More on that later.

这是一个重要的事实,因为这使我们可以分别控制每个组件的更改检测的方式和时间!后续会说明。

Let’s assume that somewhere in our component tree an event is fired, maybe a button has been clicked. What happen next? We just learned that zones execute the given handler and notify Angular when the trun is done, which eventually[最终的] causes Angular to perform change detection.

我们假设在组件树种触发了某一个事件,也许单击了一个按钮。接下来发生什么?我们前面了解到,Zones执行给定的处理程序并在执行完毕后会通知Angular,这最终使Angular执行变更检测。

Since each component has its own change detector, and an Angular application consists of[由。。。组成] a component tree, the logical[逻辑上的] result is that we’re having a change detector tree too. This tree can also be viewd as a directed[有方向的] graph when data always flows from top to bottom.

由于每个组件都有它自己的变更检测器,并且Angular应用程序是由一个组件树组成的,因此在逻辑上,我们也有一个变更检测器的树。该树也可以视为有向图,其中数据从上到下流动。

The reason why data flows from top to bottom, is because change detection is also always performed from top to bottom for every single component, every single time, starting from the root component.

数据总是从上而下的流动的原因是,从根组件开始,每次每个组件也是从上到下执行变更检测。

This is awesome, as unidirectional[单向] data flow is more predictable[可预测的] than cycles. We always know where the data we use in our views comes from, because it can only result from its component.

这很好,因为单项数据流比循环数据流更好预测。我们始终知道我们在视图中使用的数据来自何处,因为它只能由其组件产生。

Another interesting observation is that change detection gets stable after a single pass.

另一个有趣的观察是,变更检测在单次通过后会变得稳定。

Meaning that, if one of our components causes any additional side effects after this first run during change detection, Angular will throw an error.

这意味着,如果我们的某个组件在变更检测期间的第一次运行后引起任何附加作用,则Angular会抛出错误。

Performance 性能

By default, event if we have to check every single component every single time an event happens, Angular is very fast. It can perform hundreds of thousands of checks within a couple of milliseconds. This is mainly due to the fact that Angular generates VM friendly code.

默认情况下,即使每次事件发生时我们都必须检查每个组件,Angular也非常快,它可以在几毫秒内执行数十万次检查。这主要是由于Angualr生成了VM友好的代码。

What does that mean? Well, When we said that each component has its own change detector, it’s not like there’s this single generic thing in Angular that takes care of change detection for each individual component.

这意味着什么?当我们说每个组件都有自己的变更检测器时,并不是说Angular中有一个通用的东西要为每个单独的组件进行变更检测。

The reason for that is, that it has to be written in a dynamic way, so it can check every component no matter what its model structure looks like.

这是因为它必须以动态方式编写,因此,无论其模型结构如何,它都可以检查每个组件。

VM don’t like the sort of dynamic code, because they cant’t optimize it. It’s considered polymorphic as the shape of the objects isn’t always the same.

VM不喜欢这种动态代码,因为它无法对其进行优化。它被认为是多态的,因为对象的结构并不总是相同的。

Angular creates change detector classes at runtime for each component, which are monomorphic[单态的], because they know exactly what the shape of the component’s model is.

Angular在运行时会为每个组件创建变更检测器的类,它是单态的,因为它们确切的知道了组件模型的形状。

VMs can perfectly optimize this code, which makes it very fast to execute. The good thing is that we don’t have to care about that too much, because Angular does it automatically.

VM可以完美的优化这些代码,这使得执行速度非常快。好处是我们不必关心太多,因为Angular会自动处理。

Smarter Change Detection 更智能的变更检测

Again, Angular has to check every component every single time an event happens because… well, maybe the application state has changed. But wouldn’t it be great if we could tell Angular to only run change detection for the parts of the application that changed their state?

在重复一次,Angular在事件发生时必须检查每个组件,因为。。。嗯,应用程序的状态可能已经发生更改。但是,如果我们可以告诉Angular仅对更改状态的部分运行变更检测,那不是很好么?

Yes it would, and in fact we can! It turns out there are data structures that give us some guarantees[保障] of when something has changed or not - Immutables and Observables. If we happen to use these structures or types, and we tell Angular about it, change detection can be much much faster. OKay cool, but how so?

是的,事实上我们可以!事实证明,有一些数据接口可以使我们对某些事物何时发生更改有所保障(不可变类型和可观察类型)。如果我们碰巧使用了这些数据结构或类型,并且在其变更的时候其告诉Angular,则变更检测会快的多。很酷,但是这是为何?

Understanding Mutability 了解可变性

In order to understand why and how e.g. immutable data structures can help, we need to understand what mutability means. Assume we have the following component.

为了理解如何以及为什么不可变的数据结构会更好一点,我们需要了解可变性的含义。假设我们有如下的组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component({
template: '<v-card [vData]="vData"></v-card>',
})
class VCardApp {

constructor() {
this.vData = {
name: 'Christoph Burgdorf',
email: 'christoph@thoughtram.io'
};
}

changeData() {
this.vData.name = 'Pascal Precht';
}
}

VCardApp uses <v-card> as a child component, which has an input property vData. We’re passing data to that component with VCardApp‘s own vData property. vData is an object with two properties. In addition, there’s method changeData(), which changes the name of vData. No magic going on here.

VCardApp使用的<v-card>做为一个子组件,子组件有一个输入项vData, 我们通过VCardApp的属性vData给子组件传值。vData是一个拥有两个属性的对象。此外,它拥有一个方法changeData()用来改变vData对象的name属性的值。这里并没有什么高深的用法。

The important is that changeData() mutates vData by changing its name property. Even though that property is going to be changed, the vData reference itself stays the same.

重要的是changeData()方法通过更改vDataname属性来对其进行变更,即使该属性被更改,vData的引用本身也保持不变。

What happens when change detection is performed, assuming that some event causes changeData() to be executed? First, vData.name gets changed, and then it’s passed to <v-card>. <v-card>‘s change detector now checks if the given vData is still the same as before, and yes, it is. The reference hasn’t changed. However, the name property has changed, so Angular will perform change detection for that object nonetheless[尽管如此].

假设某些事件导致changeData()函数被执行,俺么在执行变更检测时会发生什么?首先,更改vData.name,然后将其传递给<v-card><v-card>的变更检测器将检查给定的vData是否和以前的相同,是的,没有改变。但是name属性已更改,因此Angular仍将对该对象执行更改检测。

Because objects are mutable by default in JavaScript(except for primitives), Angular has to be conservative and run change detection every single time for every component when an event happens.

因为对象在JavaScript中是可变的(除过原始值外),Angular必须保持保守的态度,并且在事件发生时每次对每个组件运行变更检测。

This is where immutable data structures come into play.

这就是不可变数据发挥作用的地方。

Immutable Objects 不可变对象

Immutable objects give us the guarantee that objects can’t change. Meaning that, if we use immutable objects and we want to make a change on such an object, we’ll always get a nwe reference with that change, as the original object is immutable.

不可变对象给了我们对象不可变的保障。意味着,如果我们使用了不可变对象,并且希望对此类对象进行更改,则由于该原始对象是不可变的,因此,我们总是会获得与该更改相关的新的引用的对象。

This pseudo code demonstrates it.

这个伪代码演示:

1
2
3
4
5
6
7
const vData = someApiForImmutable.create({
name: 'Pascal Precht',
});

const vData2 = vData.set('name', 'tony');

vData === vData2 // false

someApiForImmutables can be any Api we want to use for immutable data structures. However, as we can see, we can’t simply change the name property. We’ll get a new object with that particular change and this object has a new reference. Or in shot: If there’s a change, we get a new reference.

someApiForImmutable可以是任何一个用来创建不可变数据结构的Api。但是,正如我们看到的,我们无法简单的更改name属性。我们将获得具有特定改变的新对象,简而言之:如果有变更,我们将获得新的引用。

Reducing the number of checks 减少检测的数量

Angular can skip entrie change detection subtrees when input properties don’t change. We just learned that a “change” means “new reference”. If we use immutable objects in Angular app, all we need to do is tell Angular that a component can skip change detection, if its input hasn’t changed.

当输入属性不发生变化时,Angular可以跳过整个变更检测的子树。我们刚刚了解到,”更改“意味着”新的引用“。如果我们在Angular项目中使用不可变对象,我们要做的就是告诉Angular组件可以跳过变更检测(如果其输入未更改)。

Let’s see how that wooks by taking a look at <v-card>:

1
2
3
4
5
6
7
8
9
@component({
template: `
<h2>{{vData.name}}</h2>
<span>{{vData.email}}</span>
`
})
class VCardCmp {
@Input() vData;
}

As we see, VCardCmp only depends on its input properties. Great. We can tell Angular to skip change detection for this component’s subtree if none of its inputs changed by setting the change detection strategy to OnPush like this:

正如我们所见,VCardCmp组件只依赖于它的输入项。很好,我们可以通过设置它的变更检测机制为OnPush,来告诉Angular如果该组件的子树的输入属性没有发生更改就跳过变更检测:

1
2
3
4
5
6
7
8
9
10
@component({
template: `
<h2>{{vData.name}}</h2>
<span>{{vData.email}}</span>
`,
changeDetection: ChangeDetectionStategy.OnPush,
})
class VCardCmp {
@Input() vData;
}

Observables 可观察对象

As mentioned earlier, Obserables also give us some certain guarantees of when a change has happened. Unlike immutable objects, they don’t give us new references when a change is made. Instead, they fire events we can subscribe to in order to react to them.

正如前文所述,可观察对象同样给我们提供了发生变化时的某些保障。与不可变对象不同,它们在发生变更时不会给我们提供新的引用。但是它们会发出我们可以订阅的事件以对其作出反应。

To understand what that means, let’s take a look at this component:

1
2
3
4
5
6
7
8
9
10
11
12
@Component({
template: `{{counter}}`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
class CartBadgeCmp {
@Input addItemStream: Observable<any>;
counter: number = 0;

ngOnInit() {
this.addItemStream.subscribe(() => this.counter ++);
}
}

Let’s say we build an e-commerce application with a shopping cart. Whenever a user puts a product into the shopping cart, we want a little counter to show up in our UI, so the user can see the amount of products in the cart.

假设我们正在构建一个由购物车组成的电子商务应用。当用户将产品放入了购物车时,我们希望在UI中显示一个柜台,以便于用户可以看到购物车中的产品数量。

CartBadgeCmp does exactly that. It has a counter and an input property addItemStream, which is a stream of events that gets fired, whenever a product is added to the shopping cart.

CartBadgeCmp正是这样做的。它拥有一个counter和一个输入属性addItemStream。每当将产品加入购物车时,该属性就会触发事件流。

In addition[此外], we set the change detection stategy to OnPush, so change detection isn’t performed all the time, only when the component’s input properties change.

此外,我们将该组件的变更检测策略设置为了OnPush模式。所以变更检测并不是每次都执行的,只有在该组件的输入属性变化时才执行。

However, as mentioned earlier, the reference of addItemStream will nave change, so change detection is never performed for this component’s subtree. This is a problem because the component subscribe to that stream in its ngOnInit life cycle hook and increments the counter. This is application state change and we want to have this reflected in our view right?

但是,如前文所述,addItemStream的引用永远不会更改,因此永远不会对此组件的子树执行变更检测。这是一个问题,因为该组件在其ngOnInit的生命周期狗子中订阅了该流,并增加了计数器。这是应用程序状态更改,我们希望在我们的视图中也发生变化吗?

We can access component’s ChangeDetectorRef via dependency injection, which comes with an Api called markForCheck(). This method does exactly what we need! It marks the path from our component until root to be checked for the next change detection run.

我们可以通过依赖注入来访问组件的ChangeDetectorRef,该注入带有一个名为markForCheck()的API。这个方法完全满足我们的需求。它标记了从组件到根组件的路径,以检查下一次更改更改检测运行。

Let’s inject it into our component and tell Angular to mark the path from this component until root to be checked:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component({
template: `{{counter}}`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
class CartBadgeCmp {
@Input addItemStream: Observable<any>;
counter: number = 0;


constructor(
private cd: ChangeDetectorRef,
) {}

ngOnInit() {
this.addItemStream.subscribe(() => {
this.counter ++;
this.cd.markForCheck();
});
}
}

Boom, that’s it!

变更检测详解,同时也提高下英文文档的阅读能力,对变更检测的机制了解更多一点。