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启动时执行逻辑的抽象代码片段。

  1. function zoneAwareAddEventListener() {...}
  2. function zoneAwareRemoveEventListener() {...}
  3. function zoneAwarePromise() {...}
  4. function patchTimeout() {...}
  5. window.prototype.addEventListener = zoneAwareAddEventListener;
  6. window.prototype.removeEventListener = zoneAwareRemoveEventListener;
  7. window.prototype.promise = zoneAwarePromise;
  8. window.prototype.setTimeout = patchTimeout;

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

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

让我们看一个实例

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

  1. // 加载zone.js
  2. Zone.current.fork({}).run(function () {
  3. Zone.current.inTheZone = true;
  4. setTimeout(function () {
  5. console.log('in the zone: ' + !!Zone.current.inTheZone);
  6. }, 0);
  7. });
  8. console.log('in the zone: ' + !!Zone.current.inTheZone);

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

  1. 'in the zone: false'
  2. 'in the zone: true'

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

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

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

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

在Zone中执行的函数

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

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

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

Zone之外的log语句

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

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

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

Zone的初始化和收尾代码

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

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

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

  1. //加载Zone.js 这会给所有的浏览器时间打上补丁
  2. Zone.current.fork({}).run(function () {
  3. // 初始化Zone
  4. // 触发器: run函数被调用了。首先会初始化zone然后才会执行后续逻辑
  5. // 动作:
  6. // - Zone.current被设置为函数被执行时所属的Zone。
  7. // 在这里,它就是我们fork根Zone生成的那个。
  8. // 我们就叫它exampleZone吧。
  9. // - Zone的生命周期里的钩子函数会被触发(我们稍后会继续讨论)
  10. // Zone.current上会多一个布尔值属性。在经历了zone的初始化过程之后
  11. // 此时的Zone.current指向的是exampleZone
  12. Zone.current.inTheZone = true;
  13. // 这里注册了一个定时器。由于被打过了猴子补丁,这里调用的并不是
  14. // 浏览器"默认"的timeout方法。因此,这里实际上是在配置代理。这里
  15. // 要重点指出的是这个代理会保留一个指向创建时所属Zone(这里就是
  16. // 'exampleZone')的引用,稍后会用到这个引用。
  17. setTimeout(
  18. ...., 0);
  19. // 销毁Zone
  20. // 触发器: 要在Zone中执行的函数已经执行完成
  21. // 动作:
  22. // - Zone.current属性被重置为根Zone
  23. // - Zone的生命周期里的钩子函数会被触发
  24. });
  25. // log语句。Zone.current属性目前指向的根Zone。
  26. // 由于它并不知晓'inTheZone'属性,因此会输出false
  27. console.log('in the zone: ' + !!Zone.current.inTheZone);
  28. // 任务栈被清空了然后定时器的回调函数开始执行
  29. // 初始化Zone
  30. // 触发器: 被打过猴子补丁的事件被触发了。proxy的包装器会触发一次
  31. // Zone的初始化。要记得proxy包装器保留了一个指向其被创建时所属
  32. // Zone的引用。
  33. // 行为:
  34. // - Zone.current属性被设置为exampleZone
  35. // - Zone的生命周期里的钩子函数会被触发
  36. function () {
  37. // exampleZone包含'inTheZone'属性,因此会输出true
  38. console.log('in the zone: ' + !!Zone.current.inTheZone);
  39. }
  40. // 销毁Zone
  41. // 触发器: 定时器回调函数执行完毕,proxy要执行一次Zone的销毁流程
  42. // 行为:
  43. // - Zone.current属性会被重置为根Zone
  44. // - Zone的生命周期里的钩子函数会被触发

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

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

Angular 2是如何利用Zone的?

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

  1. ....
  2. new NgZoneImpl({
  3. trace: enableLongStackTrace,
  4. onEnter: () => {
  5. // console.log('ZONE.enter', this._nesting, this._isStable);
  6. this._nesting++;
  7. if (this._isStable) {
  8. this._isStable = false;
  9. this._onUnstable.emit(null);
  10. }
  11. },
  12. onLeave: () => {
  13. this._nesting--;
  14. // console.log('ZONE.leave', this._nesting, this._isStable);
  15. this._checkStable();
  16. },
  17. setMicrotask: (hasMicrotasks: boolean) => {
  18. this._hasPendingMicrotasks = hasMicrotasks;
  19. this._checkStable();
  20. },
  21. setMacrotask: (hasMacrotasks: boolean) => { this._hasPendingMacrotasks = hasMacrotasks; },
  22. onError: (error: NgZoneError) => this._onErrorEvents.emit(error)
  23. });
  24. ....

这段代码来自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. 解决Hibernate4执行update操作,不更新数据的问题

    后台封装java对象,使用hibernate4再带的update,执行不更新数据,不报错. 下面贴出解决方法: 失败的方法 hibernate自带update代码:(失效) Session sessi ...

  2. vi/vim命令

    vi / vim是Unix / Linux上最常用的文本编辑器而且功能非常强大.

  3. sql compare options

    sql compare project's options Add object existence checks Use DROP and CREATE instead of ALTER Ignor ...

  4. 动态更改Menu

    好像没有现成的api可能获取menu完美方法,只有在创建menu时,用全局的menuItem记下, 在需要修改时修改. 1)全局量: MenuItem  gMenuItem=NULL; 2)//创建菜 ...

  5. C# 函数的传值与传址(转)

    http://www.cnblogs.com/mdnx/archive/2012/09/04/2671060.html using System; using System.Collections.G ...

  6. 【POJ 1679】 The Unique MST

    [题目链接] 点击打开链接 [算法] 先求出图的最小生成树 枚举不在最小生成树上的边,若加入这条边,则形成了一个环,如果在环上且在最小生成树上的权值最大的边等于 这条边的权值,那么,显然最小生成树不唯 ...

  7. ECS服务器配置密钥登录及常用日志

    一.介绍 1.SSH(22端口)是Secure Shell Protocol的简写,由IETF网络工作小组(Network Working Group)制定:在进行数据传输之前,SSH先对联机数据包通 ...

  8. UVaLive 6680 Join the Conversation (DP)

    题意:给出n条发言,让你求最大的交流长度并输出标记顺序. 析:这个题要知道的是,前面的人是不能at后面的人,只能由后面的人at前面的,那就简单了,我们只要更新每一层的最大值就好,并不会影响到其他层. ...

  9. 洛谷 P4180 【模板】严格次小生成树[BJWC2010]【次小生成树】

    严格次小生成树模板 算法流程: 先用克鲁斯卡尔求最小生成树,然后给这个最小生成树树剖一下,维护边权转点权,维护最大值和严格次大值. 然后枚举没有被选入最小生成树的边,在最小生成树上查一下这条边的两端点 ...

  10. 10.23NOIP模拟题

    叉叉题目描述现在有一个字符串,每个字母出现的次数均为偶数.接下来我们把第一次出现的字母 a 和第二次出现的 a 连一条线,第三次出现的和四次出现的字母 a 连一条线,第五次出现的和六次出现的字母 a ...