Angular 源码解析 - Zone.js

Wenzhao,Angularsource codeChineseZone.js

Angular 源码解析系列。这篇文章有关于 Zone.js 的用途,实现和 NgZone 的实现,以及 Angular 如何使用 Zone.js 实现自动变更检测。


这篇文章是 Angular 源码解析系列的第一篇,分为以下三个小节:

阅读这篇文章你需要熟悉 JavaScript 的事件循环 (Event Loop) 机制 (opens in a new tab)

现在 Zone.js 的源码已经被放到 Angular 底下以 mono repo 的形式管理,你可以通过下面的链接阅读 Zone.js 的源码。

angular/angular (opens in a new tab)

为什么 Angular 需要 Zone.js?

所有前端框架需要解决的一个共同问题就是:应该何时将应用状态的变化反映到视图中,即变更检测。React 的方案是交给用户自行决定,即让用户通过 setState 方法告诉 React 应用的状态发生了改变;Vue 通过拦截对象的赋值操作来监测状态改变(即所谓响应式);而 Angular 的方案就是 Zone.js。Zone.js 通过给一些会触发异步事件 API 打补丁(monkey patch),比如 XHR、DOM event、定时器等来监听异步事件的编排(比如调用 setTimeout)和触发(比如 setTimeout 到时),而应用状态的变化一定是某个异步事件的结果,这样 Angular 就可以借助 Zone.js 实现变更检测。

体现在代码中:Angular 应用会在 zone 的 onMicrotaskEmpty 回调中调用 tick 方法 (opens in a new tab),而 tick 方法会调用顶层组件的 detectChanges 方法执行变更检测,就是下面这行代码:

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

可以通过看代码注释 (opens in a new tab)来了解官方对 Zone.js 作用的描述。

Zone.js 如何工作?

编程模型

把 Zone.js 中的 zone 想象成 JavaScript VM 线程里的 mini 线程。

JavaScript 是单线程,基于事件循环的,而通过 Zone.js,我们可以把处理事件的回调函数放到不同的 zone 里面执行,而且在当前回调函数内触发的异步事件也会在当前 zone 里面得到处理,即我们给事件的回调函数提供了执行环境。而且,Zone.js 还提供了钩子,允许我们在回调函数执行前后执行额外一些代码(还有其他的一些钩子)。

总而言之——

zone 提供了 JavaScript 异步函数的执行环境。

核心代码

核心部分实现了 Zone.js 的机制,而不关心各种 patch 该如何实现,代码都在 zone.ts (opens in a new tab) 当中,前面几百行都是接口声明,请自行阅读,本文主要聚焦于其实现 (opens in a new tab)

重要类型和方法

这个文件主要声明和实现了如下几个类:

官方文档中对这些类所实现的接口有非常详细的注释,可以比较下面的内容阅读。

Zone

这些代码是对 Zone 的实现 (opens in a new tab)

有几个值得关注的静态方法:

这个类的构造函数 (opens in a new tab),要点如下:

Zone 类还有如下的实例方法:

在阅读代码的时候我们经常能看到 _currentZoneFrame 这个变量,这实际上上记录了一个 zone 的栈(以链表的形式),如果某个 zone 中执行了函数调用,该 zone 就进入这个栈中,这样就将函数的调用栈与进入和离开 zone 的先后顺序对应起来了。

该变量被声明在创建 Zone.js 全局对象的函数里(即在一个闭包中),外部没有办法直接访问。

ZoneDelegate

代码在这里 (opens in a new tab)

要点:

ZoneTask

这个类的代码在这里 (opens in a new tab)

要点:

除了上面几个重要的类,下面两个方法也值得关注:

scheduleMicroTask (opens in a new tab),这个方法是对 Promise 这样的所谓 micro task 的处理。

drainMicroTaskQueue (opens in a new tab)。该方法内容十分简单,即尝试对 _microTaskQueue 中的每一个 MicroTask run 一下。

补丁的实现

我们之前提到了 __load_patch 方法是 Zone.js 用来加载补丁的,这一小节我们将以 setTimeout 为例介绍 Zone.js 如何加载补丁。同时还会结合上一小节的内容,讲解 setTimeout 在 Zone.js 执行的全过程。因为各个 JavaScript runtime 对异步函数的支持情况不尽相同(比如在 Node.js 环境里不可能有 DOM 事件相关的异步函数,如 addEventListener),所以 Zone.js 会给不同的 runtime 提供不同的 dist 包,patch 不同的异步函数。好在不管在任何环境中,setTimeout 都是存在的。

patch 的具体实现在这里 (opens in a new tab),下面我们将会仔细讲解这部分代码。

首先准备好要 patch 的函数的名称:

setName += nameSuffix // setTimeout
cancelName += nameSuffix // clearTimetout

接下来,调用 patchMethod 方法,传入的三个参数分别是目标对象(被 patch 后的函数应当挂载在目标对象上,因为 setTimeout 其实是 window 的一个属性,所以这里的形参叫做 window),被 patch 函数的名字,以及一个回调函数,请注意这个回调函数直接返回了另外一个函数(如果你了解什么叫做柯里化,应该很容易理解,其实就是让该函数的执行时能够访问到 delegate 参数):

patchMethod(window, setNmae, (delegate: Function) => function(self: any, args: any[]): void);

来看 patchMethod (opens in a new tab) 方法:

export function patchMethod(
  target: any,
  name: string,
  patchFn: (
    delegate: Function,
    delegateName: string,
    name: string
  ) => (self: any, args: any[]) => any
): Function | null

首先 (opens in a new tab),该方法从 target 的原型链上找到 name 代表的方法的具体位置(不要忘记 JavaScript 访问对象属性是通过原型链机制进行的),如果找不到这个方法,就直接在 target 上创建 patch 过的方法.

然后 (opens in a new tab),检查 patch 过的方法是否存在,不存在才进行 patch。

在进行 patch 时:

用户执行 setTimeout 后会发生什么?

我在这里给读者准备了一个简单的例子 (opens in a new tab),通过给 console.log('z') 这一行打断点,就能够看到整个调用栈。

debugger

Angular 如何使用 Zone.js?

Angular 应用初始化过程中,实例化了一个 NgZone 然后将所有逻辑都跑在该对象的 _inner 对象中 (opens in a new tab)_inner 即为 Angular zone (opens in a new tab)

Angular 创建该 zone 的过程中传入的 ZoneSpec 的部分如下所示:

onHasTask:
        (delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) => {
          delegate.hasTask(target, hasTaskState);
          if (current === target) {
            // We are only interested in hasTask events which originate from our zone
            // (A child hasTask event is not interesting to us)
            if (hasTaskState.change == 'microTask') {
              zone.hasPendingMicrotasks = hasTaskState.microTask;
              checkStable(zone);
            } else if (hasTaskState.change == 'macroTask') {
              zone.hasPendingMacrotasks = hasTaskState.macroTask;
            }
          }
        },

checkStable 这个方法中你可以看到这样一行:

zone.onMicrotaskEmpty.emit(null)

这就联系到了开篇提到的代码:

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

当然 checkStable 方法 (opens in a new tab)还有可能在其他时机被调用,主要是通过 Zone.js 的 onInvokeTaskonInvoke 两个钩子,即在异步事件触发时,交给读者自行验证,这里就不赘述了。

结论

这篇文章介绍了 Zone.js 的实现,包括 Zone.js 核心和 patch 的实现,还讲解了 Angular 对 Zone.js 的使用。

参考资料

, CC BY-NC 4.0 © Wenzhao.