15个例子熟练异步框架 Zone.js
一、理解 Zone.js
可以把 Zone.js 理解成异步的监听器,通过钩子感知异步的各个阶段:
| 钩子 | 对应阶段 |
|---|---|
| onScheduleTask | 订阅/注册(调用 setTimeout、.then 等) |
| onInvokeTask | 执行(回调真正运行) |
| onCancelTask | 取消(clearTimeout 等) |
| onHasTask | 是否有未完成任务(全部完成时 hasTask 变为 false) |
核心价值:
- 提取冗余代码:错误处理、耗时统计、日志等可集中到 Zone 钩子,业务回调只保留核心逻辑
- 共享变量:用
properties在 Zone 上挂数据,Zone.current.get('key')即可访问,无需闭包或层层传参 - 统一错误捕获:
onHandleError可捕获 Zone 内同步和异步抛出的错误 - 等所有异步完成:
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 前后自己维护。
有 Zone:onHasTask 自动感知「有任务」或「全部完成」。
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 里的错误。
有 Zone:onHandleError 统一捕获 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 何时执行。
有 Zone:onInvokeTask 在每次异步回调执行前都会触发。
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 触发
});
讲解:clearTimeout、clearInterval 取消任务时,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 |
properties | Zone 携带的数据 |