15个例子熟练异步框架 Zone.js

22 阅读5分钟

15个例子熟练异步框架 Zone.js

一、理解 Zone.js

可以把 Zone.js 理解成异步的监听器,通过钩子感知异步的各个阶段:

钩子对应阶段
onScheduleTask订阅/注册(调用 setTimeout、.then 等)
onInvokeTask执行(回调真正运行)
onCancelTask取消(clearTimeout 等)
onHasTask是否有未完成任务(全部完成时 hasTask 变为 false)

核心价值:

  1. 提取冗余代码:错误处理、耗时统计、日志等可集中到 Zone 钩子,业务回调只保留核心逻辑
  2. 共享变量:用 properties 在 Zone 上挂数据,Zone.current.get('key') 即可访问,无需闭包或层层传参
  3. 统一错误捕获onHandleError 可捕获 Zone 内同步和异步抛出的错误
  4. 等所有异步完成onHasTask 在 hasTask 变为 false 时,表示全部完成,可触发回调(类似自动版 Promise.all)

官方总结

  • Zone.js 通过包装异步 API,在订阅、执行、取消、完成等阶段提供钩子,让你可以集中处理错误、上下文、监控和“全部完成”等逻辑,从而减少重复代码并提高可读性。

白话文总结:

  • Zone.js 就是一个异步“监听器”,可以追踪异步任务的执行取消注册/订阅等各个阶段,把原本分散在异步代码里的冗余处理(比如日志、错误捕获、耗时统计)提取到统一的位置,让业务代码更清晰。
  • 当“异步套异步”时,各层 Zone 可以共享变量,无需再用闭包或层层传参。
  • 支持在异步任务中统一捕获错误,不用再每处手动 try/catch。
  • 可以检测多个异步任务何时全部完成,比如多个 loading 结束后再统一触发某些操作。

二、示例精华(01-15)

01 最基本用法

// Zone.current 获取当前 Zone
console.log('当前 Zone:', Zone.current.name);

// zone.run() 在 Zone 内执行代码
Zone.current.run(function() {
  console.log('在 Zone 内执行,当前 Zone:', Zone.current.name);
});

讲解:Zone.js 加载后自动创建 root Zone。zone.run(fn) 在指定 Zone 内执行函数。


02 Zone 嵌套

var childZone = Zone.current.fork({ name: 'child-zone' });
var grandchildZone = childZone.fork({ name: 'grandchild-zone' });

childZone.run(function() {
  console.log('在 child-zone 内:', Zone.current.name);
  grandchildZone.run(function() {
    console.log('在 grandchild-zone 内:', Zone.current.name);
  });
});

讲解zone.fork(config) 基于当前 Zone 创建子 Zone,形成父子层级关系。


03 Zone 存储数据

var myZone = Zone.current.fork({
  name: 'my-zone',
  properties: {
    userId: 'user-123',
    requestId: 'req-456'
  }
});

myZone.run(function() {
  console.log('同步:', Zone.current.get('userId'));
  setTimeout(function() {
    // 异步回调里也能拿到!
    console.log('异步:', Zone.current.get('requestId'));
  }, 500);
});

讲解properties 让 Zone 携带数据,同步和异步代码都能用 Zone.current.get('key') 访问。


04 对比:上下文数据

无 Zone:多层 setTimeout 需闭包或层层传参才能拿到 requestId。

有 Zone:在 Zone 上设置一次,所有异步回调都能直接拿到。

var myZone = Zone.current.fork({
  name: 'request-zone',
  properties: { requestId: 'req-002' }
});

myZone.run(function() {
  setTimeout(function() {
    setTimeout(function() {
      // 照样能拿到,不用传参!
      console.log(Zone.current.get('requestId'));
    }, 200);
  }, 200);
});

05 对比:任务追踪

无 Zone:需手动 pendingCount++/--,每次 setTimeout 前后自己维护。

有 ZoneonHasTask 自动感知「有任务」或「全部完成」。

var trackingZone = Zone.current.fork({
  name: 'tracking-zone',
  onHasTask: function(delegate, current, target, hasTaskState) {
    var hasTask = hasTaskState.macroTask || hasTaskState.microTask || hasTaskState.eventTask;
    // hasTask 为 true:有异步任务
    // hasTask 为 false:全部完成
    console.log(hasTask ? '有任务执行中' : '空闲');
  }
});

trackingZone.run(function() {
  setTimeout(function() {
    setTimeout(function() { /* 什么都不用做 */ }, 300);
  }, 500);
});

06 对比:错误捕获

无 Zone:try-catch 抓不到 setTimeout 里的错误。

有 ZoneonHandleError 统一捕获 Zone 内所有异步错误。

var errorZone = Zone.current.fork({
  name: 'error-zone',
  onHandleError: function(delegate, current, target, error) {
    console.log('捕获到:', error.message);
    return false; // 不继续向外抛
  }
});

errorZone.run(function() {
  setTimeout(function() {
    throw new Error('setTimeout 里的错误!');
  }, 300);
});

07 对比:任务拦截

无 Zone:无法知道 setTimeout、Promise.then 何时执行。

有 ZoneonInvokeTask 在每次异步回调执行前都会触发。

var interceptZone = Zone.current.fork({
  name: 'intercept-zone',
  onInvokeTask: function(delegate, current, target, task, applyThis, applyArgs) {
    console.log('▶ 执行任务:', task.source);
    return delegate.invokeTask(target, task, applyThis, applyArgs);
  }
});

interceptZone.run(function() {
  setTimeout(function() { /* ... */ }, 200);
  Promise.resolve().then(function() { /* ... */ });
});

注意:onInvokeTask 在回调执行前触发,不是执行后。delegate.invokeTask() 会同步执行回调,执行完才返回。


08 onScheduleTask vs onInvokeTask

var z = Zone.current.fork({
  name: 'demo',
  onScheduleTask: function(delegate, curr, target, task) {
    console.log('📋 任务被注册:', task.source);  // 调用 setTimeout 的瞬间
    return delegate.scheduleTask(target, task);
  },
  onInvokeTask: function(delegate, curr, target, task, applyThis, applyArgs) {
    console.log('▶ 任务即将执行:', task.source);  // 回调真正运行的瞬间
    return delegate.invokeTask(target, task, applyThis, applyArgs);
  }
});

z.run(function() {
  setTimeout(function() { console.log('回调执行了'); }, 500);
});
// 顺序:onScheduleTask → (500ms) → onInvokeTask → 回调

讲解:onScheduleTask = 注册时;onInvokeTask = 执行时。


09 zone.wrap

var myZone = Zone.current.fork({
  name: 'my-zone',
  properties: { requestId: 'req-999' }
});

// 包装后,无论何时何处被调用,都会在 my-zone 内执行
var wrappedCallback = myZone.wrap(function() {
  console.log(Zone.current.get('requestId'));
}, 'button-callback');

document.getElementById('btn').addEventListener('click', wrappedCallback);

讲解:适合 addEventListener、第三方库回调等,你无法控制调用时机,但希望它在你的 Zone 内执行。


10 异步耗时统计

var timingZone = Zone.current.fork({
  name: 'timing-zone',
  onInvokeTask: function(delegate, curr, target, task, applyThis, applyArgs) {
    var start = performance.now();
    var result = delegate.invokeTask(target, task, applyThis, applyArgs);
    var cost = (performance.now() - start).toFixed(2);
    console.log(task.source + ' 耗时: ' + cost + ' ms');
    return result;
  }
});

讲解delegate.invokeTask() 是同步的,执行完才返回,所以前后 performance.now() 的差值就是回调耗时。


11 onInvoke 同步钩子

var z = Zone.current.fork({
  name: 'invoke-zone',
  onInvoke: function(delegate, curr, target, callback, applyThis, applyArgs, source) {
    console.log('onInvoke: 即将执行', source);
    return delegate.invoke(target, callback, applyThis, applyArgs, source);
  }
});

z.run(function() {
  console.log('zone.run 里的回调体执行了');
}, null, null, 'main');

讲解onInvoke 针对 zone.run(fn)同步执行;onInvokeTask 针对异步任务。


12 onCancelTask

var z = Zone.current.fork({
  onCancelTask: function(delegate, curr, target, task) {
    console.log('❌ 任务被取消:', task.source);
    return delegate.cancelTask(target, task);
  }
});

z.run(function() {
  var id = setTimeout(function() {}, 3000);
  // 点击按钮时 clearTimeout(id) → onCancelTask 触发
});

讲解clearTimeoutclearInterval 取消任务时,onCancelTask 会触发。


13 模拟 Angular 变更检测

var ngZone = Zone.current.fork({
  name: 'ng-zone',
  onHasTask: function(delegate, curr, target, hasTaskState) {
    delegate.hasTask(target, hasTaskState);
    var hasTask = hasTaskState.macroTask || hasTaskState.microTask || hasTaskState.eventTask;
    if (!hasTask) {
      console.log('🔔 所有异步完成 → 执行变更检测');
    }
  }
});

ngZone.run(function() {
  setTimeout(function() {
    // 更新数据...
    setTimeout(function() { /* 后续处理 */ }, 200);
  }, 500);
});

讲解:Angular 的 NgZone 就是利用 onHasTask,在「全部完成」时触发变更检测。


14 Zone 边界

// Zone 内发起 → 会被追踪
trackingZone.run(function() {
  setTimeout(function() { /* 会被 onInvokeTask 捕获 */ }, 200);
});

// Zone 外发起 → 不会被追踪
setTimeout(function() { /* 不会被 Zone 追踪! */ }, 400);

讲解:只有在 Zone 内发起的异步才会被追踪。Zone 外调用的 setTimeout 不会被感知。


15 zone.runGuarded

var safeZone = Zone.current.fork({
  onHandleError: function(delegate, curr, target, error) {
    console.log('捕获:', error.message);
    return false; // 不继续向外抛
  }
});

safeZone.runGuarded(function() {
  throw new Error('故意的错误!');
});
console.log('程序继续运行');

讲解zone.run(fn) 抛错会向外冒泡;zone.runGuarded(fn) 会捕获错误交给 onHandleError,不向外抛。


三、核心概念速查

概念说明
Zone.current当前所在的 Zone
zone.run(fn)在指定 Zone 内执行函数
zone.runGuarded(fn)安全执行,错误交给 onHandleError
zone.fork(config)基于当前 Zone 创建子 Zone
zone.wrap(callback)包装回调,使其在 Zone 内执行
Zone.current.get('key')获取 Zone 的 properties
propertiesZone 携带的数据

四、码云地址

码云地址gitee.com/leeyamaster…