Skip to content

Latest commit

 

History

History
183 lines (132 loc) · 10.4 KB

angular-40.[翻译]-Angular的状态变更机制并不一定依赖于NgZone(zone.js).md

File metadata and controls

183 lines (132 loc) · 10.4 KB

[翻译] Angular的状态变更机制并不一定依赖于NgZone(zone.js)

原文:Do you still think that NgZone (zone.js) is required for change detection in Angular? 作者:Max Koretskyi 原技术博文由 Max Koretskyi 撰写发布,他目前于 ag-Grid 担任开发者职位。

译者按:开发大使负责确保其所在的公司认真听取社区的声音并向社区传达他们的行动及目标,其作为社区和公司之间的纽带存在。 译者:秋天;校对者:Sunny Liu

Zone.js

本篇文章的主题不是 Zones(zone.js),而是关注 Angular 中的 Zone 实现:NgZone 以及他们在状态变更检测机制中的联系。有关于 Zone的相关解释请查看 翻阅源码后,我终于理解了Zone.js

大部分我看过的文章都把 Zone(Zone.js)NgZone 与 Angular 的状态变更检测机制强行关联在一起,虽然他们之间确实存在关联,但是从技术角度来讲,他们并不是一个整体。在发生异步事件时,ZoneNgZone 被用来自动触发变更检测,但是 Angular 中变更检测是一个独立的机制,当 ZoneNgZone 不存在时,变更检测工作仍然可以执行。第一节,将会展示在移除 zone.js 的情况下,如何使用 Angular;第二节介绍 AngularNgZone 如何通过 zone 建立起交互;最后一节会展示为什么在使用某些第三方库,例如 Google API Client Library 的情况下,自动变更检测机制会失效。

我之前已经编写了很多 Angular 变更检测相关的文章,这篇文章也是这个系列的最后一篇。如果你寻求理解变更检测的工作机制,那么建议从These 5 articles will make you an Angular Change Detection expert 入手。

使用没有依赖 Zone(zone.js) 的 Angular

想要证明 Angular 在没有依赖 Zone 的情况下也能够正常运行,起初我打算准备模拟 zone 对象,并且不让这个对象做任何事情,但是在 Angular v5 版本中,Angular 提供了类似的机制。Angular 现在提供了一个方式,它使用配置 noop Zone 可以达到类似效果。

首先,要做的事情是从依赖中移除 zone.js,我将以 stackbliz 来演示这段程序,并且从 polyfils.ts 文件中删除下面的 import

* Zone JS is required by Angular itself. */
import 'zone.js/dist/zone';  // Included with Angular CLI.

随后,配置 Angular 使用 noop Zone 实现:

platformBrowserDynamic()
    .bootstrapModule(AppModule, {
        ngZone: 'noop'
    });

现在你运行这个应用程序,你将会发现变更检测仍然正常运行,并且在 DOM 中渲染了 HelloComponentname 属性。

现在添加 setTimeout 事件来更新 name 属性:

export class AppComponent  {
    name = 'Angular 5';
    constructor() {
        setTimeout(() => {
            this.name = 'updated';
        }, 1000);
    }

你会发现 name 属性在 1s 之后不会在 DOM 中更新,NgZone 没有被使用,变更检测也没有被自动触发。不过,我们可以通过手动触发变更检测,首先注入 ApplicatinoRef,随后触发 tick 方法来启动变更检测:

export class AppComponent  {
    name = 'Angular 4';

    constructor(app: ApplicationRef) {
        setTimeout(()=>{
            this.name = 'updated';
            app.tick();
        }, 1000);
    }

现在可以发现name 属性可以正常的渲染到 Dom 了。

上面的这些示例主要展示了 NgZonezone.js 并不是 Angular 状态变更检测的一部分,他们只是一种非常便捷的机制,可以自动调用 app.tick() 方法,实现自动触发状态变更。下面一节将讲述,这个自动触发变更检测的方法是如何做到的。

NgZone 是如何使用 Zones 的

上一篇讲述 Zone(zone.js) 的文章里,我深入讲述了 Zone 的工作机制以及其提供的 API。其中核心的概念是 forking a zone 和在指定的 zone 中运行一个任务。我将在后面提到这些概念。

在那篇文章中,我还提到了 Zone 的两种能力:content propagation(传递上下文) 和 outstanding asynchronous tasks tracking(追踪未完成的任务)。Angular 实现了 NgZone 类,这个类深度依赖于任务追踪机制。

NgZone 其实只是对 forked child zone 的一个包装:

function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
    zone._inner = zone._inner.fork({
        name: 'angular',
        ...

这个 forked zone 保存在 _inner 属性中,它通常被认为是 Angularzone,这个 zone 是在调用 NgZone.run() 方法时,用来执行回调:

run(fn, applyThis, applyArgs) {
    return this._inner.run(fn, applyThis, applyArgs);
}

继承于 Angular zonecurrent zone 被保存在 _outer 属性中,它被用来在执行 NgZone.runOutsideAngular() 时调用一个回调:

runOutsideAngular(fn) {
    return this._outer.run(fn);
}

这个方法常用来在 Angular zone 之外运行耗性能操作,以避免不断地触发变更检测。

NgZone 有一个 isStable 属性,用来表示当前是否存在未完成的宏任务(macro task)和微任务(micro task)。此外它还定义了四个事件:

Event Description
onUnstable 当任务代码进入到了 Angular zone 中给出通知,它在事件循环周期中首先被激活。
onMicrotaskEmpty 当微任务队列为空时给出通知,它用来提示 Angular 做变更检测,并安排更多的微任务进入队列。也正是这个原因,这个方法也会在事件循环周期中被执行多次。
onStable 在最近的 onMicrotaskEmpty 运行的时候给出通知,代表当前微任务队列为空,事件循环周期可以放弃当前循环。这个事件仅会被执行一次。
onError 当接收到错误时,发出通知。

AngularApplicationRef 中使用 onMicrotaskEmpty 事件来自动地触发变更检测:

constructor(...) {
  this._zone.onMicrotaskEmpty.subscribe(
    {next: () => { this._zone.run(() => { this.tick(); }); }});
}

NgZone 是如何实现 onMicrotaskEmpty 事件的

一起看看 NgZone 是如何实现 onMicrotaskEmpty 事件的。这个事件由 checkStable 函数发出:

function checkStable(zone: NgZonePrivate) {
  if (zone._nesting == 0 && !zone.hasPendingMicrotasks && !zone.isStable) {
    try {
      zone._nesting++;
      zone.onMicrotaskEmpty.emit(null); <-------------------

此外这个功能周期性地被 Zone 的钩子方法调用:

  • onHasTask
  • onInvokeTask
  • onInvoke

onHasTask 钩子方法是当全部队列执行完毕后执行检测,onInvokeTask 是异步任务执行回调时触发,onInvoke 是代码进入到 zone 时触发。第一个钩子 onHasTask 是针对整个任务队列状态改变监听,后两个钩子 onInvokeTaskonInvoke 是针对单个任务状态改变的监听。

常见的陷阱

在 stackoverflow 中最常见的问题之一是在使用某些第三方库的时候,变更检测机制失效。例如使用 gapi(Google API Client Library) 库时的例子。通用的解决方案,是在 Angular 的 zone 中执行一个回调:

gapi.load('auth2', () => {
    zone.run(() => {
        ...

然而,一个有趣的问题是,为什么 zone 没有捕捉到这个请求,导致 zone 的钩子没有发出通知,从而 NgZone 也就无法自动地触发变更检测。

为了探求这个原因,我查看了 gapi 压缩后的源码,发现它使用了 JSONP 技术来发起网络请求,它并没有使用 AJAX API,例如 XMLHttpRequest 或者 Fetch API,而这些 API 已经被 zone 打补丁了。相反,它会创建一个带有源 URL 的脚本,并定义了一个全局回调,当从服务器获取数据时,会触发这个全局回调。这个行为 zone 并没有打补丁,因而也就无法被 zone 监听,因而 Angular 也无法得知这个请求,从而无法自动触发变更检测。

下面就是从压缩后的 gapi 中截取的"奇怪"的处理代码:

Ja = function(a) {
    var b = L.createElement(Z);
    b.setAttribute(“src”, a);
    a = Ia();
    null !== a && b.setAttribute(“nonce”, a);
    b.async = “true”;
    (a = L.getElementsByTagName(Z)[0]) ? 
        a.parentNode.insertBefore(b, a) : 
        (L.head || L.body || L.documentElement).appendChild(b)
}

z 变量等同于 scripta 参数等同于 带有源的 URL地址:

https://apis.google.com/_.../cb=gapi.loaded_0

URL 最后的 gapi.loaded_0 代码就是全局的回调:

typeof gapi.loaded_0 
"function"