zone.js - 暴力之美

在ng2的开发过程中,Angular团队为我们带来了一个新的库 – zone.js。zone.js的设计灵感来源于Dart语言,它描述JavaScript执行过程的上下文,可以在异步任务之间进行持久性传递,它类似于Java中的TLS(thread-local storage: 线程本地存储)技术,zone.js则是将TLS引入到JavaScript语言中的实现框架。

那么zone.js能为我们解决什么问题呢?在回答这个问题之前,博主更希望回顾下在JavaScript开发中,我们究竟遇见了什么难题?

问题引入

我们先来看一段常规的同步JavaScript代码:

var foo = function(){ ... },
    bar = function(){ ... },
    baz = function(){ ... };

foo();
bar();
baz();

这段代码并没有什么特殊之处,它的执行顺序也并无什么特殊之处,完全在我们的预知之内:foo –> bar –> baz。对它做性能监测也很容易,我们只需要在执行上下文前后记录执行时间即可。

var start,
    timer = performance ? performance.now.bind(performance) : Date.now.bind(Date);

start = timer();

foo();
bar();
baz(); 

console.log(Math.floor((timer() - start) * 100) / 100 + 'ms');

但在JavaScript的世界并不全是这么简单,众所周知的JavaScript单线程执行的。因此为了不阻塞UI界面的用户体验,在JavaScript执行的很多耗时操作都被封装为了异步操作,如:setTimeout、XMLHttpRequest、DOM事件等。由于浏览器的寄宿限制,JavaScript中异步操作是与生俱来的特性,被深深的印在了骨髓之中。这也是Ryan Dahl博士选择JavaScript开发Node.js平台的原因之一。关于JavaScript单线程执行可以参考博主的另一篇博文:JavaScript单线程和浏览器事件循环简述

那么对于下面这段异步代码,我们又如何做性能监测呢?

var foo = function(){ setTimeout(..., 2000); },
    bar = function(){ $.get(...).success(...); },
    baz = function(){ ... };

foo();
bar();
baz();

在这段代码中,引入了setTimeout和AJAX异步调用。其中AJAX回调和setTimeout回调时间顺序很难确定,因此给这段代码引入性能检测代码并不像上面的顺序执行代码一样那么简单了。如果我们需要强行加入性能的检测,则会在setTimeout和$.get回调中插入相关的hook代码并并记录执行时间,这样我们的业务代码也会变得非常混乱,就像一团“意大利拉面”一样(What the fuck!)。

zone.js简介

在本文开篇提到zone.js为JavaScript提供了执行上下文,可以在异步任务之间进行持久性传递。该是zone.js上场的时候了。zone.js采用猴子补丁(Monkey-patched)的暴力方式将JavaScript中的异步任务都包裹了一层,使得这些异步任务都将运行在zone的上下文中。每一个异步的任务在zone.js都被当做为一个Task,并在Task的基础上zone.js为开发者提供了执行前后的钩子函数(hook)。这些钩子函数包括:

  • onZoneCreated:产生一个新的zone对象时的钩子函数。zone.fork也会产生一个继承至基类zone的新zone,形成一个独立的zone上下文;
  • beforeTask:zone Task执行前的钩子函数;
  • afterTask:zone Task执行完成后的钩子函数;
  • onError:zone运行Task时候的异常钩子函数;

并且zone.js对JavaScript中的大多数异步事件都做了包裹封装,它们包括:

  • zone.alert;
  • zone.prompt;
  • zone.requestAnimationFrame、zone.webkitRequestAnimationFrame、zone.mozRequestAnimationFrame;
  • zone.addEventListener;
  • zone.addEventListener、zone.removeEventListener;
  • zone.setTimeout、zone.clearTimeout、zone.setImmediate;
  • zone.setInterval、zone.clearInterval

以及对promise、geolocation定位信息、websocket等也进行了包裹封装,你可以在这里找到它们https://github.com/angular/zone.js/tree/master/lib/patch

下面我们先来看一个简单的zone.js示例:

var log = function(phase){
    return function(){
        console.log("I am in zone.js " + phase + "!");
    };
};

zone.fork({
    onZoneCreated: log("onZoneCreated"),
    beforeTask: log("beforeTask"),
    afterTask: log("afterTask"),
}).run(function(){
    var methodLog = function(func){
        return function(){
            console.log("I am from " + func + " function!");
        };
    },
    foo = methodLog("foo"),
    bar = methodLog("bar"),
    baz = function(){
        setTimeout(methodLog('baz in setTimeout'), 0);
    };

    foo();
    baz();
    bar();
});

执行这段示例代码的输出是:

I am in zone.js beforeTask!
I am from foo function!
I am from bar function!
I am in zone.js afterTask!

I am in zone.js onZoneCreated!
I am in zone.js beforeTask!
I am from baz in setTimeout function!
I am in zone.js afterTask!

从上面的输出结果,我们能够看出在zone.js中将run方法块分为了两个Task,它们分别是方法体运行时的Task和异步setTimeout的Task。并且我们能够在这些Task的创建,执行前后拦截并做一些有意义的事情。

在zone.js中fork方法会产生一个继承至zone的子类,并在fork函数中可以配置特定的钩子方法,形成独立的zone上下文。而run方法则是启动执行业务代码的对外接口。

同时zone也支持父子继承,以及它也定义了一套DSL语法,支持$、+、-的前缀。

  • $会传递父类zone的钩子函数,便于对zone钩子函数执行的控制;
  • -代表在父zone的钩子函数之前运行本钩子函数;
  • +则与之相反,代表在父zone的钩子函数之后运行本钩子函数

更多的语法使用,请参考zone.js github首页文档https://github.com/angular/zone.js

引入zone.js

有了上面的这些关于zone.js的基础知识,在本文开始的遗留问题我们就可以迎刃而解了。下面这段代码是来自zone.js项目的示例代码:https://github.com/angular/zone.js/blob/master/example/profiling.html

var profilingZone = (function () {
    var time = 0,
        timer = performance ?
                    performance.now.bind(performance) :
                    Date.now.bind(Date);
    return {
      beforeTask: function () {
        this.start = timer();
      },
      afterTask: function () {
        time += timer() - this.start;
      },
      time: function () {
        return Math.floor(time*100) / 100 + 'ms';
      },
      reset: function () {
        time = 0;
      }
    };
  }());

  zone.fork(profilingZone).run(function(){

     //业务逻辑代码

  });

这里在beforeTask中启动了时间计算,并在afterTask中计算出当前累积的花费的时间。因此我们在业务代码的逻辑中就可以随时利用zone.time()来获取当前耗时了。

zone.js的实现

了解了zone.js的时候之后,或许你会像我一样感觉很神奇,它是如何实现的呢?

下面是zone.js中browser.ts的代码片段(https://github.com/angular/zone.js/blob/master/lib/patch/browser.ts):

export function apply() {
  fnPatch.patchSetClearFunction(global, global.Zone, [
    ['setTimeout', 'clearTimeout', false, false],
    ['setInterval', 'clearInterval', true, false],
    ['setImmediate', 'clearImmediate', false, false],
    ['requestAnimationFrame', 'cancelAnimationFrame', false, true],
    ['mozRequestAnimationFrame', 'mozCancelAnimationFrame', false, true],
    ['webkitRequestAnimationFrame', 'webkitCancelAnimationFrame', false, true]
  ]);

  fnPatch.patchFunction(global, [
    'alert',
    'prompt'
  ]);

  eventTargetPatch.apply();

  propertyDescriptorPatch.apply();

  promisePatch.apply();

  mutationObserverPatch.patchClass('MutationObserver');
  mutationObserverPatch.patchClass('WebKitMutationObserver');

  definePropertyPatch.apply();

  registerElementPatch.apply();

  geolocationPatch.apply();

  fileReaderPatch.apply();
}

从这里我们能看到,zone.js对浏览器中的setTimeout、setInterval、setImmediate、以及事件、promise、地理信息geolocation都做了特殊处理。那么这些处理是怎么处理的呢?下面是关于fnPatch.patchSetClearFunction的实现代码,来自zone.js中functions.ts(https://github.com/angular/zone.js/blob/master/lib/patch/functions.ts)的代码片段:

export function patchSetClearFunction(window, Zone, fnNames) {
  function patchMacroTaskMethod(setName, clearName, repeating, isRaf) {
    //浏览器原生方法留存
    var setNative = window[setName];
    var clearNative = window[clearName];
    var ids = {};

    if (setNative) {
      var wtfSetEventFn = wtf.createEvent('Zone#' + setName + '(uint32 zone, uint32 id, uint32 delay)');
      var wtfClearEventFn = wtf.createEvent('Zone#' + clearName + '(uint32 zone, uint32 id)');
      var wtfCallbackFn = wtf.createScope('Zone#cb:' + setName + '(uint32 zone, uint32 id, uint32 delay)');

      // 对浏览器原生方法的包裹封装
      window[setName] = function () {
        return global.zone[setName].apply(global.zone, arguments);
      };

      // 对浏览器原生方法的包裹封装
      window[clearName] = function () {
        return global.zone[clearName].apply(global.zone, arguments);
      };

      // 创建自己包裹方法,由上面的wind[setName]转移到这里执行.
      Zone.prototype[setName] = function (fn, delay) {

        var callbackFn = fn;
        if (typeof callbackFn !== 'function') {
          // force the error by calling the method with wrong args
          setNative.apply(window, arguments);
        }
        var zone = this;
        var setId = null;
        // wrap the callback function into the zone.
        arguments[0] = function() {
          var callbackZone = zone.isRootZone() || isRaf ? zone : zone.fork();
          var callbackThis = this;
          var callbackArgs = arguments;
          return wtf.leaveScope(
              wtfCallbackFn(callbackZone.$id, setId, delay),
              callbackZone.run(function() {
                if (!repeating) {
                  delete ids[setId];
                  callbackZone.removeTask(callbackFn);
                }
                return callbackFn.apply(callbackThis, callbackArgs);
              })
          );
        };
        if (repeating) {
          zone.addRepeatingTask(callbackFn);
        } else {
          zone.addTask(callbackFn);
        }
        setId = setNative.apply(window, arguments);
        ids[setId] = callbackFn;
        wtfSetEventFn(zone.$id, setId, delay);
        return setId;
      };
      ......

    }
  }
  fnNames.forEach(function(args) {
    patchMacroTaskMethod.apply(null, args);
  });
};

在上面的代码中,首先会将浏览器的原生方法保存在setNative中以便将会重用。紧接着zone.js就开始了它的暴力行为,覆盖window[setName]和window[clearName]然后将对setName的调用转到自身的zone[setName]的调用,zone.js就是如此暴力的对浏览器原生对象实现了拦截转移。然后它会在Task执行的前后调用自身的addRepeatingTask、addTask以及wtf事件来应用注册上的所有钩子函数。

到这里相信作为读者的你已经明白了zone.js的实现机制了,是不是和笔者一样有种“简单粗暴”的感觉?但是它真的很强大,为我们实现了对异步Task的跟踪、分析等。

zone.js应用场景

zone.js能实现异步Task跟踪,分析,错误记录、开发调试跟踪等,这些都是zone.js场景的应用场景。你也可以在https://github.com/angular/zone.js/tree/master/example看见更多的示例代码,以及Brian在ng-conf 2014关于zone.js的演讲视频: https://www.youtube.com/watch?v=3IqtmUscE_U.

当然对于一些特定的业务分析zone.js也有它很好的运用场景。如果你使用过Angular1的开发,那么也许你还能记忆犹新的想起:使用第三方事件或者ajax却忘记$scope.$apply的场景吧。在Angular1中如果在非Angular的上下文改变数据Model,Angular是无法预知的,因此也不会触发界面的更新。所以我们不得不显示的调用$scope.$apply或者$timeout来触发界面的更新。Angular框架为了更多的获知变化的事件,不得不为封装了一整套框架内置的服务和指令,如ngClick、ngChange、$http,$timeout等,这也增加了Angular1的学习成本。

也是为了解决Angular1的这一些列问题,Angular2团队引入了zone.js,放弃自定义这类服务和指令,相反而是拥抱浏览器的原生对象和方法。所以在Angular2中可以使用浏览器的任何事件了,只需要括号模板语法的标识:(eventName),等价于on-eventName;也可以直接使用浏览器的原生对象了,如setTimeout,addEventListener、promise、fetch等。

当然,zone.js也能应用于Angular1的项目之中。示例代码如下(http://jsbin.com/kenilivuvi/edit?html,js,output):

angular.module("com.ngbook.demo", [])
    .controller("DemoController", ['$scope', function($scope){

        zone.fork({
            afterTask: function(){
                var phase = $scope.$root.$$phase;
                if(['$apply', '$digest'].indexOf(phase) === -1) {
                    $scope.$apply();
                 }
            }
        }).run(function(){

            setTimeout(function(){
                $scope.fromZone = "I am from zone with setTimeout!";
            }, 2000);
        });

    }]);

在示例代码中,在每次Task的完成后都会尝试$scope.$apply,强制将Model数据的改变更新到UI界面。对于在Angular1中使用zone.js更多的地方应该是在Directive中,同时也可以将zone的创建过程封装为服务(工厂方法,每次返回一个全新的zone对象)。在Angular2中也有同样zone的封装,它被称为ngZone(https://github.com/angular/angular/blob/master/modules/angular2/src/core/zone/ng_zone.ts)。

作者:破  狼 
出处:http://www.cnblogs.com/whitewolf/ 
本文版权归作者,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。该文章也同时发布在我的独立博客中-个人独立博客博客园--破狼51CTO--破狼。http://www.cnblogs.com/whitewolf/p/zone-js.html

时间: 2024-11-10 00:39:05

zone.js - 暴力之美的相关文章

jQuery列表框插件imageselect.js jquery select美化用图片进行选择

 select默认的太难看了,在这用jquery 插件imageselect.js进行美化,就图片进行下拉菜单的选择 列表框插件imageselect.js jquery select美化用图片进行选择-jquery select 美化"> 这个网页特效代码如下:   <!DOCTYPE html> <head> <title>支持图片选择的jQuery列表框插件imageselect.js</title> <meta http-equ

Zone.js —— JavaScript 的线程本地存储技术

Zone.js 在 JavaScript 中实现了 Zones 的概念,其灵感来自于 Dart. Zone 是执行过程的上下文,可以在异步任务之间进行持久性传递,你可以把它当成是类似 Java 的 TLS 线程本地存储技术,只不过是用在 JavaScript 语言中. 文章转载自 开源中国社区 [http://www.oschina.net]

暴力与美的结合 - 子弹切面“解剖照”揭秘其内部精密构成

class="post_content" itemprop="articleBody"> 澳大利亚的摄影师萨冰•派尔曼(Sabine Pearlman)最近创作了大量子弹竖切面的特写作品,并举办了名为"弹药"的个人展览,每一幅照片当中都展现出被切成两半的子弹内部复杂精细的构造和组成-- 这次展出的900多颗子弹于都是2012年10月在瑞典境内的"二战"战壕内被发现的.这位艺术家用镜头捕捉到了弹壳里隐藏着的精细而又致命的美

backbone.js学习实例

着手开始学习 什么是backbone.js? 美公的理解是 一种js的mvc的框架,分为Model(模型),View(视图)和Collection(集合),如果有mvc分层开发经验的话,会容易理解. 为什么学习这个? 因为用他可以在的单个页面完成多个应用模块,给用户的感觉是不用刷新页面,适合webapp开发 $(function(){ var testModel = Backbone.Model.extend({ defaults:{ id:"1", name:'meigong', a

"生活大爆炸"等下架 美剧制作公司将推中国版

<生活大爆炸> <傲骨贤妻> 近日,<生活大爆炸>.<傲骨贤妻>等多部美剧在各大视频网站下架,用户在登录观看页面时,会看到"因政策等原因暂时无法提供观看服务"的提示. 据悉,此次被下架的美剧涉及优酷.搜狐视频.腾讯视频等网站.其中,搜狐视频拥有<生活大爆炸>的独家版权,而<傲骨贤妻>的版权则分属搜狐视频.优酷和腾讯视频.除了上述两部美剧,遭下架的美剧还包括<海军罪案调查处>和<律师本色>.

多数网站暂缓买美剧转购日韩剧,广电总局重申“先审后播”

大批热门美剧下架视频网站:严格按规定执行4月26日,不少网友反映,已经播出到第七季情景喜剧<生活大爆炸>遭下架.此外美剧<傲骨贤妻>.<海军罪案调查处>以及<律师本色>等热门剧也遭全网下架,下架页面上显示:"很抱歉,因政策等原因,暂时无法提供观看服务".27日,华西都市报记者分别致电拥有下架美剧版权的几家视频网站,对方均回应:"是严格按照相关制度规定执行的." 80%的美剧或将下架对于<生活大爆炸>等美剧的

热点推荐:2015前端生态发展回顾

处在2015年这个时间段来看,前端生态已经进入了第三阶段.看上去好像已经走的挺远了,实则不然.如果再用人类历史上的三次工业革命来类比,前端 发展其实不过刚刚迈入了蒸汽机时代,开始逐步用工具来替代过往相当一部分的人肉作业,但是离电气时代的自动化流水线作业还有很长一段路要走.回顾一下 2015年前端的生态发展,我大致整理了几个我觉得比较有历史意义的事件. 按时间顺序: 年初 React Native 的发布,引领 React 正式走上历史舞台. 3月 angular2.0 第一个预览版发布 5月 h

angularjs2.0指南

AngularJS 2.0 学习 Angular 2 当越来越多的 web app 使用 Angular 1构建的时候,更快更强大的 Angular 2 将会很快成为新的标准. Angular的新约定使得它更容易去学习.更快的去开发 app.通过本教程学习更快速.更强大的 Angular 版本. Angular 一个跨移动和桌面的框架 快速开始 本指南指导你如何构建一个简单 Angular app. 可以使用typescript/ JavaScript / Dart任意一种语言来编写Angula

Angular 1 vs. Angular 2 深度比较

AngularJS 2 尽管还在Alpha阶段,但主要功能和文档已经发布.让我我们了解下Angular 1 和 2 的区别,以及新的设计目标将如何实现. Angular 2 当前仍处于 Alpha/开发预览阶段,但是主要功能和核心文档都已经可用了.让我们一起了解下 Angular 2 的设计目标,以及实现它们的计划: Angular 2 主要目标 更易于推论 Angular 1 vs Angular 2 变化侦测 基于 Zones 的更透明的内部构件 改进的堆栈跟踪 大幅提升的性能 (以及原理)