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)。

时间: 2024-10-17 02:33:59

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

Zone.js.jar各版本下载地址搜集汇总

Zone.js-0.7.2.jar http://www.dsjkf.cn/jar/5028.html Zone.js-0.6.26.jar http://www.dsjkf.cn/jar/5029.html Zone.js-0.5.15.jar http://www.dsjkf.cn/jar/5030.html

JS 暴力禁止alert弹窗

写代码时,弹窗问题经常非常困扰.单列一篇随笔吧: // 禁止alert弹窗. 防止错误提醒 window.alert = function() { return false; } 参考:https://blog.csdn.net/qq_38334525/article/details/79621177 原文地址:https://www.cnblogs.com/pu369/p/12336873.html

Angularjs2-zone.js:1382 GET http://localhost:3000/traceur 404 (Not Found)

zone.js:1382 GET http://localhost:3000/traceur 404 (Not Found) 按照 angularjs2 英雄指南,磕磕绊绊走到了http这一步,然后被这个错误秒杀了. 解决方案: In systemjs.config.js you should change the mapping to: 'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-w

【JavaScript】前端开发框架三剑客—AngularJS VS. Backone.js VS.Ember.js

摘要:透过对Github,StackOverflow,YouTube等社区进行数据收集后可知,AngularJS在各大主流社区中都是最受欢迎的,Backbone.js与Ember.js则不相伯仲.本文将对当前三款流行的Web开发框架作个简单比较. 是否选择了合适的框架进行Web开发对项目是有重大影响的.我们都希望找到一个稳健的易维护的框架结构.接下来,我们一起来对当前三款流行Web开发框架作个简单比较认识. 初步认识 AngularJS诞生于2009年,是商业产品GetAngular的一部分.后

AngularJS 、Backbone.js 和 Ember.js 的比较

1 介绍 我们准备在这篇文章中比较三款流行于Web的“模型-视图-*”框架:AngularJS.Backbone和Ember.为你的项目选择正确的框架能够对你及时交付项目的能力和在以后维护你自己代码的能力产生巨大影响.你也许想基于一款可靠的.稳定的和成熟的框架来构建项目,但又不想为此受到约束.Web发展迅速——新技术产生,旧的那套方法很快跟不上潮流.如此形势之下,我们准备仔细深入的比较这三个框架. 2  框架概览 今天我们提到的所有框架有许多共同点:都是开源的,遵从 MIT 协议,并且都尝试通过

在项目生成时就合并压缩你的js

对网站优化来讲,合并压缩js.css等静态内容是必修课之一,一则可以节省宽带:二则可以减少http请求:三则加快了网站的访问速度 对于如何实现合并压缩js 方案一: 继承IHttpModule ,对http进行拦截,然后获取其<script src=*> tag,将js文件取出来合并成为一个资源文件 方案二:如果你用的web解决方案为 asp.net mvc 4,那么当然可以用Bundle技术. 方案三:不依赖任何服务器技术,本地先合并压缩好后再上传 本文着重讲解方案三 要合并文件,可以用do

node.js与ThreadLocal

ThreadLocal变量的说法来自于Java,这是在多线程模型下出现并发问题的一种解决方案. ThreadLocal变量作为线程内的局部变量,在多线程下可以保持独立,它存在于 线程的生命周期内,可以在线程运行阶段多个模块间共享数据.那么,ThreadLocal变量 又如何与node.js扯上关系呢? node模型 node的运行模型无需再赘言: "事件循环 + 异步执行",可是node开发工程师比较感兴趣的点 大多集中在 "编码模式"上,即异步代码同步编写,由此提

第二届ngChina开发者大会来了!

2019 ngChina 开发者大会将于11月23-24日在杭州举办!届时,全国各地前端和全栈开发的小伙伴们齐聚杭州,一起零距离聆听来自中国.中国台湾.美国.德国.以色列.英国.奥地利.保加利亚.日本.马来西亚等国家和地区的专家们带来的互联网技术最新动态和深度主题分享.本次大会更是准备了黑客马拉松,让你的2天杭州之行一定收获满满. 今年的 ngChina 大会请到了比去年多一倍的讲师,其中有十位谷歌认证的开发者专家(Google Developer Expert,简称 GDE),包括NestJS

[Step-By-Step Angular2](1)Hello World与自动化环境搭建

随着rc(release candidate,候选版本)版本的推出,万众瞩目的angular2终于离正式发布不远啦!五月初举办的ng-conf大会已经过去了整整一个月,大多数api都如愿保持在了相对稳定的状态——当然也有router这样的例外,在rc阶段还在大面积返工,让人颇为不解——不过总得说来,现在学习angular2不失为一个恰当的时机. Google为angular2准备了完善的文档和教程,按理说,官网(https://angular.io)自然是学习新框架的最好教材.略显遗憾的是,在B