Zone.js到底是如何工作的?

原文链接: blog.kwintenp.com

如果你阅读过关于Angular 2变化检测的资料,那么你很可能听说过zone。Zone是一个从Dart中引入的特性并被Angular 2内部用来判断是否应该触发变化检测。

如果你去到zone.js的GitHub页面,你会发现它对Zone是这么定义的:

Zone是一个在异步任务间保持一致的执行环境。你可以把它理解成是JavaScript VM的线程本地存储。

第一次读到这句话你可能会像我一样摸不着头脑。为了更好的理解它的含义,我推荐你观看Brian Ford在ngConf 2014上的这个演讲并阅读thoughtram上的这篇理解zones

然而,即使是在观看了演讲并阅读了博客文章以后,我还是对它实际的工作原理很好奇。Zone.js是如何给浏览器事件打上猴子补丁,那些github页面上的例子又到底是如何工作的呢。本文旨在把我在调查过程中学到的知识分享出来。

浏览器事件是如何被打上猴子补丁的,这又意味着什么呢?

为了了解浏览器事件是如何被打上猴子补丁的,我决定深入源码。以下是Zone.js启动时执行逻辑的抽象代码片段。

function zoneAwareAddEventListener() {...}
function zoneAwareRemoveEventListener() {...}
function zoneAwarePromise() {...}
function patchTimeout() {...}
window.prototype.addEventListener = zoneAwareAddEventListener;
window.prototype.removeEventListener = zoneAwareRemoveEventListener;
window.prototype.promise = zoneAwarePromise;
window.prototype.setTimeout = patchTimeout;

注意: zone.js实际上给更多的事件打了补丁,由于原理相同在此处不一一列出。

原来zone.js覆写了一些window原型上的函数,换之以一些代理函数。这意味着在加载zone.js脚本之后出发的任何事件或是创建的任何promise都是被代理函数封装过的。这个概念就叫做猴子补丁。

让我们看一个实例

让我们看看zone.js GitHub仓库里README文件中的第一个示例(这里是该示例的plnkr

// 加载zone.js
Zone.current.fork({}).run(function () {
Zone.current.inTheZone = true; setTimeout(function () {
console.log('in the zone: ' + !!Zone.current.inTheZone);
}, 0);
}); console.log('in the zone: ' + !!Zone.current.inTheZone);

如果执行这段代码,你会得到以下的结果:

'in the zone: false'
'in the zone: true'

你可能期望两次输出的结果都是true,因为我们在两处输出了同一个属性。

为了理解这是如何工作的,我们需要把焦点聚集到这个代码片段的某些部分上。

在一个Zone中创建并执行代码
Zone.current.fork({}).run( .... );

当zone.js被加载时,它会创建一个可以用于访问根Zone的全局属性。在这个例子中,我们通过fork根Zone Zone.current来创建一个Zone。我们在新创建的对象上执行run函数来在这个Zone内部执行某些代码。

在Zone中执行的函数

接下来让我们看看这个在Zone中执行的函数:

....
Zone.current.inTheZone = true; setTimeout(function () {
console.log('in the zone: ' + !!Zone.current.inTheZone);
}, 0);
....

这段代码首先在Zone.current属性上增加了一个布尔值。然后设置了一个定时器用来在调用栈被清空之后(如果你不太清楚我在说什么,我推荐你看看这个分享)输出这个新创建的属性。

Zone之外的log语句

最后,同样的log语句也在zone之外被执行了一次。

....
console.log('in the zone: ' + !!Zone.current.inTheZone);

我们同样访问了相同的Zone.current属性。如果我们在两条log语句中访问了同一个属性,为何输出的结果会不一样呢?

Zone的初始化和收尾代码

每次在Zone内部执行代码或是一个被打过猴子补丁的事件类型被触发时,Zone或是代理函数都会在执行函数或回调之前初始化Zone。代理函数之所以能初始化Zone是因为它保留了一个指向它被创建时所属Zone的引用。

在初始化的过程中,与这个特定Zone相关的状态都会被恢复,因此即使是定时器,事件监听器这样的异步代码执行起来也像同步的代码一样。你可以把Zone理解为一个在异步任务之间保持一致的执行环境,就像定义里说的那样。

为了进一步澄清,请看看下面这个代码片段。我把代码按照它执行的顺序重新整理并增加了初始化和收尾的时间点。注释中有更多详细信息。

//加载Zone.js 这会给所有的浏览器时间打上补丁

Zone.current.fork({}).run(function () {
// 初始化Zone
// 触发器: run函数被调用了。首先会初始化zone然后才会执行后续逻辑
// 动作:
// - Zone.current被设置为函数被执行时所属的Zone。
// 在这里,它就是我们fork根Zone生成的那个。
// 我们就叫它exampleZone吧。
// - Zone的生命周期里的钩子函数会被触发(我们稍后会继续讨论) // Zone.current上会多一个布尔值属性。在经历了zone的初始化过程之后
// 此时的Zone.current指向的是exampleZone
Zone.current.inTheZone = true; // 这里注册了一个定时器。由于被打过了猴子补丁,这里调用的并不是
// 浏览器"默认"的timeout方法。因此,这里实际上是在配置代理。这里
// 要重点指出的是这个代理会保留一个指向创建时所属Zone(这里就是
// 'exampleZone')的引用,稍后会用到这个引用。
setTimeout(
...., 0); // 销毁Zone
// 触发器: 要在Zone中执行的函数已经执行完成
// 动作:
// - Zone.current属性被重置为根Zone
// - Zone的生命周期里的钩子函数会被触发
}); // log语句。Zone.current属性目前指向的根Zone。
// 由于它并不知晓'inTheZone'属性,因此会输出false
console.log('in the zone: ' + !!Zone.current.inTheZone); // 任务栈被清空了然后定时器的回调函数开始执行 // 初始化Zone
// 触发器: 被打过猴子补丁的事件被触发了。proxy的包装器会触发一次
// Zone的初始化。要记得proxy包装器保留了一个指向其被创建时所属
// Zone的引用。
// 行为:
// - Zone.current属性被设置为exampleZone
// - Zone的生命周期里的钩子函数会被触发
function () {
// exampleZone包含'inTheZone'属性,因此会输出true
console.log('in the zone: ' + !!Zone.current.inTheZone);
}
// 销毁Zone
// 触发器: 定时器回调函数执行完毕,proxy要执行一次Zone的销毁流程
// 行为:
// - Zone.current属性会被重置为根Zone
// - Zone的生命周期里的钩子函数会被触发

多亏了针对事件的猴子补丁使得Zone.js可以在执行定时器回调函数时初始化并销毁Zone。

这么解释应该清楚一些了吧!

Angular 2是如何利用Zone的?

为了了解Angular 2是如何利用Zone的,我查看以下它的源码。请看下面这个代码片段:

....
new NgZoneImpl({
trace: enableLongStackTrace,
onEnter: () => {
// console.log('ZONE.enter', this._nesting, this._isStable);
this._nesting++;
if (this._isStable) {
this._isStable = false;
this._onUnstable.emit(null);
}
},
onLeave: () => {
this._nesting--;
// console.log('ZONE.leave', this._nesting, this._isStable);
this._checkStable();
},
setMicrotask: (hasMicrotasks: boolean) => {
this._hasPendingMicrotasks = hasMicrotasks;
this._checkStable();
},
setMacrotask: (hasMacrotasks: boolean) => { this._hasPendingMacrotasks = hasMacrotasks; },
onError: (error: NgZoneError) => this._onErrorEvents.emit(error)
});
....

这段代码来自NgZone.ts文件。Zone.js暴露了一个Zone生命周期各阶段的钩子函数。这里列出了Angular 2所监听的事件。由于Angular 2中所有的代码都在同一个Zone中执行,也就是ngZOne, 因此Angular 2可以利用它的这些回调函数来判断何时该执行一次变更检测循环。这避免了像Angular 1中那样手动调用$digest

原文链接 https://www.zcfy.cc/article/how-the-hell-does-zone-js-really-work

Angular ZoneJS 原理的更多相关文章

  1. (译) Angular运行原理揭秘 Part 1

    当你用AngularJS写的应用越多, 你会越发的觉得它相当神奇. 之前我用AngularJS实现了相当多酷炫的效果, 所以我决定去看看它的源码, 我想这样也许我能知道它的原理. 下面是我从源码中找到 ...

  2. 【转】Angular运行原理揭秘 Part 1

    当你用AngularJS写的应用越多, 你会越发的觉得它相当神奇. 之前我用AngularJS实现了相当多酷炫的效果, 所以我决定去看看它的源码, 我想这样也许我能知道它的原理. 下面是我从源码中找到 ...

  3. angular核心原理解析3:指令的执行过程

    指令的执行过程分析. 我们知道指令的执行分两个阶段,一个是compile,一个是link. 我们可以在指令中自定义compile和link. 首先,我们来讲解如何自定义link函数 举个例子: < ...

  4. angular核心原理解析2:注入器的创建和使用

    上一课没有讲到创建注入器的方法createInjector. 此方法,会创建两种不同的注入器:第一种叫做providerInjector,第二种叫做instanceInjector.providerI ...

  5. angular核心原理解析1:angular自启动过程

    angularJS的源代码整体上来说是一个自执行函数,在angularJS加载完成后,就会自动执行了. angular源代码中: angular = window.angular || (window ...

  6. 30行代码让你理解angular依赖注入:angular 依赖注入原理

    依赖注入(Dependency Injection,简称DI)是像C#,java等典型的面向对象语言框架设计原则控制反转的一种典型的一种实现方式,angular把它引入到js中,介绍angular依赖 ...

  7. angular 依赖注入原理

    依赖注入(Dependency Injection,简称DI)是像C#,java等典型的面向对象语言框架设计原则控制反转的一种典型的一种实现方式,angular把它引入到js中,介绍angular依赖 ...

  8. Angular 的性能优化

    目录 序言 变更检查机制 性能优化原理 性能优化方案 小结 参考 序言 本文将谈一谈 Angular 的性能优化,并且主要介绍与运行时相关的优化.在谈如何优化之前,首先我们需要明确什么样的页面是存在性 ...

  9. 手动启动angular

    关于手动启动 angular 的问题 angular核心原理解析1:angular自启动过程 angular.element(document).ready(function() { angular. ...

随机推荐

  1. Unix - 文件里构成一个空洞的分析

    lseek函数显示地为一个打开文件设置偏移量,文件偏移量能够大于文件的当前长度,在这样的情况下.对该文件的下一次写将加长该文件.并在文件里构成一个空洞,这一点是同意的. 位于文件里但没有写过的字节都被 ...

  2. Linux环境部署(一)

    最近被老大安排了个任务,解决Linux的安装部署问题,特做如下笔记,以便下次安装配置: --------------------Linux上部署项目------------------- 1.解压缩相 ...

  3. linux内核对块设备的使用

    1 partition table 这里的分析以经典的MBR为例. 在MBR里面有partition table,每一项对应一个逻辑的块设备,partion table中的每一项是16个字节. 第一个 ...

  4. Aaron Swartz Rewriting Reddit中关于web.py的创建思路

    这天才少年居然自杀了,哎 原文点这 So how should things work? The first principle is that code should be clear and si ...

  5. 实现SpringBoot登录

    SpringBoot登录 https://www.cnblogs.com/jiekzou/p/9303871.html 通过前面10篇文章的学习,相信我们对SpringBoot已经有了一些了解,那么如 ...

  6. [Codeforces 666B] World Tour

    [题目链接] https://codeforces.com/contest/666/problem/B [算法] 首先 , 用BFS求出任意两点的最短路径 然后 , 我们用f[i][0-2]表示从i出 ...

  7. nginx开发(一) 源码-编译

    1:获取源码 http://nginx.org/download/nginx-1.8.0.tar.gz 2:编译 解压之后,进入根目录,执行 ./configuer.sh make make inst ...

  8. 用户能够在下次登录系统时被重新配置---或win10早期更新不成功的bug就需要删除多余的登陆用户

    有时候我们希望删除本地计算机上的一位用户,或者期待一位用户能够在下次登录系统时被重新配置.但是仅删除%userprofile%目录是不够的,因为一个登录名会对应一个SSID,这个SSID还保留在注册表 ...

  9. emma中文显示乱码问题解决(ubutnu)

    vim -/.emma/emmarc 找到  db_encoding=latin1 改为  db_encoding=utf8  然后重新运行emma,此时发现还是乱码,不要着急,在执行所有的sql语句 ...

  10. springAOP配置XML方式配置切面报错error at ::0 formal unbound in pointcut

    [错误配置文件] <aop:config> <aop:pointcut expression="execution(* net.fifteenho.service.impl ...