• ZoneJS 的原理与应用


    目录

    • 序言
    • Zone 是什么
    • ZoneJS 的原理
    • ZoneJS 的应用场景
    • 参考

    1. 序言

    ZoneJS 是 Angular 团队受到 Dart 的 Zone 的启发,为 Angular v2 及其以上版本设计的核心模块。Angular 通过引入 ZoneJS 使得其变更检测机制更加简单与可靠。


    2. Zone 是什么

    在 ZoneJS 中有一个核心概念:Zone(域)。一个 Zone 表示一个 JavaScript 执行过程的上下文,其可以在异步任务之间进行持久性传递。

    Zone 是执行上下文


    先看一个示例:

    import 'zone.js'
    
    const rootZone = Zone.current;
    const zoneA = rootZone.fork({name: 'A'});
    
    expect(Zone.current).toBe(rootZone);
    
    setTimeout(function timeoutCb1() {
      // 此回调在 rootZone 中执行
      expect(Zone.current).toEqual(rootZone);
    }, 0);
    
    // 执行 run 方法,将切换 Zone.current 所保存的 Zone
    zoneA.run(function run1() { 
      expect(Zone.current).toEqual(zoneA);
    
      // setTimeout 在 zoneA 中被调用
      setTimeout(function timeoutCb2() {
        // 此回调在 zoneA 中执行
        expect(Zone.current).toEqual(zoneA);
      }, 0);
    });
    
    // 退出 zoneA.run 后,将切换回之前的 Zone
    expect(Zone.current).toBe(rootZone);
    

    在上述代码中:

    1. Zone.currentZone 上的一个静态属性,用来保存全局此刻正在使用的 Zone
    2. Zone.run() 方法将切换 Zone.current 所保存的 Zone
    3. Zones 之间的关系:

    最初的 rootZone 是 ZoneJS 默认创建的一个 Zone 实例。而通过 Zone.fork()方法,可以再创建子Zone(这也是一个 Zone 实例,因此可以继续调用 fork() 方法创建子 Zone,而其parent属性将关联创建其的父 Zone),这些 Zones 最终可以形成一个树形结构。

    const rootZone = Zone.current;
    const zoneA = rootZone.fork({name: 'A'});
    const zoneB = rootZone.fork({name: 'B'});
    const zoneC = zoneA.fork({name: 'C'});
    

    上述代码中的 Zones 之间的关系如下图所示:

    Zones 的树形结构

    从上图中也可以看出,这些 Zones 形成的树形结构是一颗有唯一根节点的树


    3. ZoneJS 的原理

    ZoneJS 通过 Monkey patch (猴补丁)的方式,暴力地将浏览器或 Node 中的所有异步 API 进行了封装替换。


    比如浏览器中的 setTimeout

    let originalSetTimeout = window.setTimeout;
    
    window.setTimeout = function(callback, delay) {
      return originalSetTimeout(Zone.current.wrap(callback),  delay);
    }
    
    Zone.prototype.wrap = function(callback) {
      // 获取当前的 Zone
      let capturedZone = this;
    
      return function() {
        return capturedZone.runGuarded(callback, this, arguments);
      };
    };
    

    或者 Promise.then方法:

    let originalPromiseThen = Promise.prototype.then;
    
    // NOTE: 这里做了简化,实际上 then 可以接受更多参数
    Promise.prototype.then = function(callback) {
      // 获取当前的 Zone
      let capturedZone = Zone.current;
      
      function wrappedCallback() {
        return capturedZone.run(callback, this, arguments);
      };
      
      // 触发原来的回调在 capturedZone 中
      return originalPromiseThen.call(this, [wrappedCallback]);
    };
    

    简单来说,ZoneJS 在加载时,对所有异步接口进行了封装,因此所有在 Zone 中执行的异步方法都会被当做为一个 Task 被其统一监管,并且提供了相应的钩子函数(hooks),用来在异步任务执行前后或某个阶段做一些额外的操作,因此可以实现:记录日志、监控性能、附加数据到异步执行上下文中等。


    而这些钩子函数(hooks),其实就是通过Zone.fork()方法来进行设置的,具体可以参考如下配置:

    Zone.current.fork(zoneSpec) // zoneSpec 的类型是 ZoneSpec
    
    // 只有 name 是必选项,其他可选
    interface ZoneSpec {
      name: string; // zone 的名称,一般用于调试 Zones 时使用
      properties?: { [key: string]: any; } ; // zone 可以附加的一些数据,通过 Zone.get('key') 可以获取 
      onFork: Function; // 当 zone 被 forked,触发该函数
      onIntercept?: Function; // 对所有回调进行拦截
      onInvoke?: Function; // 当回调被调用时,触发该函数
      onHandleError?: Function; // 对异常进行统一处理
      onScheduleTask?: Function; // 当任务进行调度时,触发该函数
      onInvokeTask?: Function; // 当触发任务执行时,触发该函数
      onCancelTask?: Function; // 当任务被取消时,触发该函数
      onHasTask?: Function; // 通知任务队列的状态改变
    }
    

    举一个onInvoke的简单列子:

    let logZone = Zone.current.fork({ 
      name: 'logZone',
      onInvoke: function(parentZoneDelegate, currentZone, targetZone, delegate, applyThis, applyArgs, source) {
        console.log(targetZone.name, 'enter');
        parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source)
        console.log(targetZone.name, 'leave'); }
    });
    
    logZone.run(function myApp() {
        console.log(Zone.current.name, 'queue promise');
        Promise.resolve('OK').then((value) => {console.log(Zone.current.name, 'Promise', value)
      });
    });
    

    最终执行结果:

    onInvoke 示例执行结果


    4. ZoneJS 的应用场景


    ZoneJS 的应用场景有很多,例如:

    • 可以用于开发调试、错误记录、分析和测试
    • 可以让框架知道什么时候可以重新渲染(在 Angular 中的应用)
    • 可以实现异步 Task 跟踪以及自动释放和清理资源
    • 等等

    这里举几个比较实用的例子:


    1. 在测试中的应用:不允许异步代码

    const syncZoneSpec = {
      name: 'SyncZone',
      onScheduleTask: function() {
        throw new Error('No Async work is allowed in test.'); // 如果存在异步任务调度,将抛出异常
      }
    }
    
    function sync(fn) {
      return function(...args) {
        Zone.current.fork(syncZoneSpec).run(fn, args, this);
      }
    }
    
    it('should fail when doing async', sync(() => { 
      Promise.resolve('value');
    }));
    

    上述实现可以用来保证测试的代码中没有异步方法被调用。


    2. 用于性能分析:监听异步方法的执行时间

    const executeTimeZoneSpec = {
      name: 'executeTimeZone',
      onScheduleTask: function (parentZoneDelegate, currentZone, targetZone, task) {
        console.time('scheduleTask')
        return parentZoneDelegate.scheduleTask(targetZone, task);
      },
      onInvokeTask: function (parentzone, currentZone, targetZone, task, applyThis, applyArgs) {
        console.time('callback')
        parentzone.invokeTask(targetZone, task, applyThis, applyArgs);
        console.timeEnd('callback')
        console.timeEnd('scheduleTask')
      }
    }
    
    Zone.current.fork(executeTimeZoneSpec).run(() => {
      setTimeout(function () {
        console.log('start callback...')
        for (let i = 0; i < 100; i++) {
          console.log(i)
        }
      }, 1000);
    });
    
    // start callback...
    // 0
    // ...
    // 100
    // callback: 12.2890625ms
    // scheduleTask: 1015.6650390625ms
    

    在 JavaScript 中类似 setTimeout 这种异步调用,其回调执行的时机很难确定,想要直接监控其执行时间一般来说是比较苦难的,而通过引入 ZoneJS 则可以很容易实现这点。


    3. 在框架中的应用:实现自动重新渲染

    class VMTurnZoneSpec {
      constructor(vmTurnDone) {
        this.name = 'VMTurnZone';
        this.vmTurnDone = vmTurnDone;
        this.hasAsyncTask = false
      }
    
      onHasTask(delegate, current, target, hasTaskState) {
        const { microTask, macroTask, eventTask } = hasTaskState
        this.hasAsyncTask = microTask || macroTask || eventTask;
        if (!this.hasAsyncTask) {
          this.vmTurnDone();
        }
      }
    
      onInvokeTask(parent, current, target, task, applyThis, applyArgs) {
        try {
          return parent.invokeTask(target, task, applyThis, applyArgs);
        } finally {
          if (!this.hasAsyncTask) {
            this.vmTurnDone();
          }
        }
      }
    }
    

    上述代码中的ZoneSpec可以用来检查异步任务是否执行完毕,然后触发对应的回调方法。而像 Angular 这种框架,正是需要知道什么时候所有的任务执行完毕以此来执行 DOM 更新(变更检测)。


    5. 参考

    学习 Zone.js

    zone.js - 暴力之美

    zone.js and NgZone

    ZoneJS in Angular

  • 相关阅读:
    原代码,反码,解释和具体的补充 Java在&gt;&gt;和&gt;&gt;&gt;差异
    开源 自由 java CMS
    Socket方法LAN多线程文件传输
    《》猿从程序书评项目经理-猿自办节目
    今年,我开始在路上
    mysql 拒绝访问的解决办法
    Mysql连接错误:Mysql Host is blocked because of many connection errors
    基于jquery的从一个页面跳转到另一个页面的指定位置的实现代码
    【转】URL编码(encodeURIComponent和decodeURIComponent)
    oracle sql日期比较
  • 原文地址:https://www.cnblogs.com/forcheng/p/13472326.html
Copyright © 2020-2023  润新知