Zone.js 源码初探

声明

本文假设读者已经了解 Zone.js 的基本用法,明白 Zone.js 需要解决的问题。需要学习或复习的读者可以参看我的另一篇文章 Zone.js 入门

初识 Zone.js

Angular 仓库下的那个 Zone.js 中的例子有点小小的问题。因此这边,我 fork了一份原作者的仓库进行学习研究。Zone.js 本身是由 TypeScript 写就,通过Gulp + Rollup 的方式进行构建,这块应用比较复杂,这里我们只需重点关注 dist 文件夹下最终编译合并后的 zone.js 。这份代码对应了源码中多个不同的 TS 文件,我们可以参考一个简单的例子,来共同探寻 Zone.js 源代码的秘密。

测试用例

理解源代码最好的方式还是写出一个测试用例,然后跟踪它。我在 zon-lab 中就建立了一个很好的例子。它的入口函数非常简单,只包含了 setTimeout,这可以让我们集中精力研究 Zone 内部的具体实现。

探索之旅!

现在让我们根据测试用例的线索,结合 Zone.js 源码,弄清其中的原理吧!

上图是 Zone.js 导入使用的简略流程图。当我们引入 Zone.js 时,脚本会自动执行 Zone 对象的构造,并使用 loadpach 方法对 Web API 打补丁(Monkey Patch)。导入完毕之后,我们使用 Zone.fork 对 Zone 进行配置,设置函数执行的生命周期钩子。最后调用 Zone.run 在 Zone 中执行我们的函数。Zone 会确保其中调用的事件函数和计时器函数执行时,能够触发配置好的生命周期钩子。

Zone 类

首先,当我们导入 Zone.js 时,会在全局创建一个名为 Zone 的类。这个创建方法在 Zone 类的构造函数中生成。

Zone 构造函数

Zone 的构造函数非常特殊,是由代表父区域的 parent(一个 Zone 对象)和 当前区域的配置信息(ZoneSpec)所指定。这里 parent 非常重要,这个引用可以让我们建立一个可嵌套的区域。子区域可以继承父区域设定的一系列钩子函数。其中常用的钩子有 OnScheduelTask、OnHasTask、OnInvokeTask 等。这三个函数我在 Zone.js 入门 中做了详细的解释。

Zone.__load_patch

Zone.__load_patch 是 Zone 中为全局对象打补丁的核心。它会把方法按照打补丁方式的不同分别进行处理。

timer 就针对 set\clearTimeout,set\clearInterval,set\clearImmediate 三个函数,利用 patchTimer 调用 patchMethod 方法进行替换。改造后的函数会利用 scheduleMacroTaskWithCurrentZone 进行调用执行。

这个函数会将我们的回调函数封装在一个名为 ZoneTask 的结构体中。其中ZoneTask 结构体绑定了当前区域中的 current 对象。这样在我们运行改造过的方法时候,会先执行区域中配置好的钩子函数,当钩子完毕全部执行完毕后再执行我们的回调函数。

Zone.current

Zone 的静态 current 成员变量可以让我们获取一个指向当前区域 Zone 的对象。这个对象会在我们调用 Zone 对象的 run 方法时,被自动置为当前区域的 Zone 的对象,使得后续执行的任务能被不同的区域所管理。

Zone.fork

这是一个实例方法。当我们调用 Zone 类对象 fork 方法时,传入区域的配置信息(由 ZoneSpec 指定)后,就交由成员变量 _zoneDelegate 进一步的处理。它是 ZoneDelegate 的一个实例对象。

Zone.run

Zone.run 是 Zone 类的一个实例方法。它的主要作用是切换当前的 Zone 的静态成员变量 current 的值,使其指向当前的区域(Zone)对象。在交给代理 _zoneDelegate 通过 invoke 调用完 run 中的函数之后,在将 current 的值进行恢复。这个方法非常巧妙地实现了不同区域下 Zone 实例对象的隔离。

相关阅读

Zone.js 入门

总结

本文根据 Zone.js 的使用方法,研究了 Zone.js 替换 setTimeout 函数的原理。

更多替换的实现方式可以参看 Zone.js 源码。

发表评论

电子邮件地址不会被公开。 必填项已用*标注