Angular ZoneJS 原理

更新日期: 2019-05-25 阅读: 4.1k 标签: 原理

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

如果你阅读过关于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文件中的第一个示例

// 加载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。

原文链接: blog.kwintenp.com


 

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/3894

相关推荐

React Native之原理浅析

讲React Native之前,了解JavaScriptCore会有帮助,也是必要的。React Native的核心驱动力就来自于JS Engine. 你写的所有JS和JSX代码都会被JS Engine来执行, 没有JS Engine的参与,你是无法享受ReactJS给原生应用开发带来的便利的

连v-show都不会你还敢说熟悉 Vue 原理?

Vue 作为最主流的前端框架,中文资料齐全、入门简单、生态活跃,可以说是工作中最常用的,如今对 Vue 原理的熟悉基本上是简历的标配了。之前参与了部分 2019 校园招聘的面试工作,发现很多简历上都写了:

js中flat方法的实现原理

Array.prototype.flat()在Array的显示原型下有一个flat方法,可以将多维数组,降维,传的参数是多少就降多少维,自定义flat的步骤1、第一步是类型判断,需要判断当前调用方法的this是否为一个数组,若不是数组则返回undefined

JavaScript 中的函数式编程原理

做了一些研究,我发现了函数式编程概念,如不变性和纯函数。 这些概念使你能够构建无副作用的功能,而函数式编程的一些优点,也使得系统变得更加容易维护。我将通过 JavaScript 中的大量代码示例向您详细介绍函数式编程和一些重要概念。

理解Vue的Watch原理

watch 是由用户定义的数据监听,当监听的属性发生改变就会触发回调,这项配置在业务中是很常用。在面试时,也是必问知识点,一般会用作和 computed 进行比较。

写一个简单的vue-router来剖析原理

随着前端业务的发展, 我们一般在写一个较为大型的vue项目时候,会使用到vue-router,来根据指定的url或者hash来进行内容的分发,可以达到不像服务端发送请求,就完成页面内容的切换,能够减少像服务器发送的请求

CSS定位之BFC背后的神奇原理

BFC已经是一个耳听熟闻的词语了,网上有许多关于 BFC 的文章,介绍了如何触发 BFC 以及 BFC 的一些用处(如清浮动,防止 margin 重叠等)。BFC直译为\"块级格式化上下文\"。它是一个独立的渲染区域,只有Block-level box参与

前端手写代码原理实现

现在的前端门槛越来越高,不再是只会写写页面那么简单。模块化、自动化、跨端开发等逐渐成为要求,但是这些都需要建立在我们牢固的基础之上。不管框架和模式怎么变,把基础原理打牢才能快速适应市场的变化。下面介绍一些常用的源码实现

关于vue过滤器的原理解析

Vue.js 允许你自定义过滤器,可被用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和 v-bind 表达式 (后者从 2.1.0+ 开始支持)。过滤器应该被添加在 JavaScript 表达式的尾部

小程序底层实现原理及一些思考

当时的我将我们的小程序定位成一个SPA单页应用 ,因为我们的小程序的宿主环境是浏览器。它只是看起来像小程序(因为这个窗口没有地址栏什么的),但其实包括UI渲染和事件交互在内的绝大部分功能都是基于Web技术

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!