当前位置: 首页 > 工具软件 > Zone.js > 使用案例 >

Angular2依赖库之zone.js

孔星宇
2023-12-01

本文翻译自Pascal Precht的《Zones in Angular2》

《Understanding Zones》一文中,我们通过创建profiling zone了解到了Zones的强大之处。我们知道了Zones 是一种执行过程的上下文,它允许我们hook into我们的异步任务。如果你还没读过该文,我们极度建议您能够先看看,因为本文是基于该文的。在本文中,我们将会进一步的了解Zones在Angular 2中扮演着什么样的角色。

Angualr的黄金搭档—Zones

It turns out that, the problem that Zones solve, plays very nicely with what Angular needs in order to perform change detection in our applications. Did you ever ask yourself when and why Angular performs change detection? What is it that tells Angular “Dude, a change probably occurred in my application. Can you please check?”.
Before we dive into these questions, let’s first think about what actually causes this change in our applications. Or rather, what can change state in our applications. Application state change is caused by three things:

  • Events - User events like click, change, input, submit, …
  • XMLHttpRequests - E.g. when fetching data from a remote service
  • Timers - setTimeout(), setInterval(), because JavaScript

It turns out that these three things have something in common. Can you name it? … Correct! They are all asynchronous.

Why do you think is this important? Well … because it turns out that these are the only cases when Angular is actually interested in updating the view. Let’s say we have an Angular 2 component that executes a handler when a button is clicked:

@Component({
  selector: 'my-component',
  template: `
    <h3>We love {{name}}</h3>
    <button (click)="changeName()">Change name</button>
  `
})
class MyComponent {

  name:string = 'thoughtram';

  changeName() {
    this.name = 'Angular';
  }
}

如果你不熟悉(click)语法,你可以去读一读我们的Angular2模板语法揭秘这篇文章。简而言之,通过(click)语法我们可以为<button>元素上的click事件设置事件处理函数。

当我们点击了该组件上的按钮,changeName函数将会执行,该组件的name属性将会被设置为Angular。我们同样希望这个改变能同时反馈到DOM上,Angular将会相应的更新视图绑定数据。

下面的例子我们将会通过使用setTimeout()来更新我们的name属性。请注意,在视图模板中,我们移除了<button>元素。

@Component({
  selector: 'my-component',
  template: `
    <h3>We love {{name}}</h3>
  `
})
class MyComponent implements OnInit {

  name:string = 'thoughtram';

  ngOnInit() {
    setTimeout(() => {
      this.name = 'Angular';
    }, 1000);
  }
}

我们根本不需要写任何额外的代码去告诉Angular框架,一个属性值改变了。不需要写ng-click, $timeout,$scope.$apply()等代码.

If you’ve read our article on understanding Zones, you know that this works obviously because Angular takes advantage of Zones. Zones monkey-patches global asynchronous operations such as setTimeout() and addEventListener(), which is why Angular can easily find out, when to update the DOM.

In fact, the code that tells Angular to perform change detection whenever the VM turn is done, is as simple as this:

ObservableWrapper.subscribe(this.zone.onTurnDone, () => {
  this.zone.run(() => {
    this.tick();
  });
});

tick() {
  // perform change detection
  this.changeDetectorRefs.forEach((detector) => {
    detector.detectChanges();
  });
}

Whenever Angular’s zone emits an onTurnDone event, it runs a task that performs change detection for the entire application. If you’re interested in how change detection in Angular 2 works, watch out, we’re going to publish another article on that soon.

But wait, where does the onTurnDone event emitter come from? This is not part of the default Zone API, right? It turns out that Angular introduces its own zone called NgZone.

NgZone in Angular 2

NgZone is basically a forked zone that extends its API and adds some additional functionality to its execution context. One of the things it adds to the API is the following set of custom events we can subscribe to, as they are observable streams:

  • onTurnStart() - Notifies subscribers just before Angular’s event turn starts. Emits an event once per browser task that is handled by Angular.
  • onTurnDone() - Notifies subscribers immediately after Angular’s zone is done processing the current turn and any micro tasks scheduled from that turn.
  • onEventDone() - Notifies subscribers immediately after the final onTurnDone() callback before ending VM event. Useful for testing to validate application state.

If “Observables” and “Streams” are super new to you, you might want to read our article on Taking advantage of Observables in Angular 2.

The main reason Angular adds its own event emitters instead of relying on beforeTask and afterTask callbacks, is that it has to keep track of timers and other micro tasks. It’s also nice that Observables are used as an API to handle these events.

Running code outside Angular’s zone

Since NgZone is really just a fork of the global zone, Angular has full control over when to run something inside its zone to perform change detection and when not. Why is that useful? Well, it turns out that we don’t always want Angular to magically perform change detection.

As mentioned a couple of times, Zones monkey-patches pretty much any global asynchronous operations by the browser. And since NgZone is just a fork of that zone which notifies the framework to perform change detection when an asynchronous operation has happened, it would also trigger change detection when things like mousemove events fire.

We probably don’t want to perform change detection every time mousemove is fired as it would slow down our application and results in very bad user experience.

That’s why NgZone comes with an API runOutsideAngular() which performs a given task in NgZone’s parent zone, which does not emit an onTurnDone event, hence no change detection is performed. To demonstrate this useful feature, let’s take look at the following code:

@Component({
  selector: 'progress-bar',
  template: `
    <h3>Progress: {{progress}}</h3>
    <button (click)="processWithinAngularZone()">
      Process within Angular zone
    </button>
  `
})
class ProgressBar {

  progress: number = 0;

  constructor(private zone: NgZone) {}

  processWithinAngularZone() {
    this.progress = 0;
    this.increaseProgress(() => console.log('Done!'));
  }
}

Nothing special going on here. We have component that calls processWithinAngularZone() when the button in the template is clicked. However, that method calls increaseProgress(). Let’s take a closer look at this one:

increaseProgress(doneCallback: () => void) {
  this.progress += 1;
  console.log(`Current progress: ${this.progress}%`);

  if (this.progress < 100) {
    window.setTimeout(() => {
      this.increaseProgress(doneCallback);
    }, 10);
  } else {
    doneCallback();
  }
}

increaseProgress() calls itself every 10 milliseconds until progress equals 100. Once it’s done, the given doneCallback will execute. Notice how we use setTimeout() to increase the progress.

Running this code in the browser, basically demonstrates what we already know. After each setTimeout() call, Angular performs change detection and updates the view, which allows us to see how progress is increased every 10 milliseconds. It gets more interesting when we run this code outside Angular’s zone. Let’s add a method that does exactly that.

processOutsideAngularZone() {
  this.progress = 0;
  this.zone.runOutsideAngular(() => {
    this.increaseProgress(() => {
      this.zone.run(() => {
        console.log('Outside Done!');
      });
    });
  });
}

processOutsideAngularZone() also calls increaseProgress() but this time using runOutsideAngularZone() which causes Angular not to be notified after each timeout. We access Angular’s zone by injecting it into our component using the NgZone token.

The UI is not updated as progress increases. However, once increaseProgress() is done, we run another task inside Angular’s zone again using zone.run() which in turn causes Angular to perform change detection which will update the view. In other words, instead of seeing progress increasing, all we see is the final value once it’s done. Check out the running code in action right here.

Zones have now also been proposed as a standard at TC39, maybe another reason to take a closer look at them.

 类似资料: