这个可以hook 异步函数,比如settimeout 等。
Async Hooks 完全详解
一、生命周期钩子详解
1. init(asyncId, type, triggerAsyncId, resource)
触发时机 :异步资源被创建的瞬间
参数详解:
javascript
init(
asyncId, // 当前异步资源的唯一ID(数字)
type, // 资源类型(字符串,如 'Timeout', 'PROMISE')
triggerAsyncId, // 触发当前资源创建的父资源ID
resource // 资源对象本身(包含具体信息)
)
执行时机示例:
javascript
// 当你执行这行代码时:
setTimeout(callback, 1000);
// ↓ 立即触发(同步)
// init(5, 'Timeout', 1, TimerObject)
// 当你创建 Promise 时:
Promise.resolve();
// ↓ 立即触发
// init(6, 'PROMISE', 1, PromiseObject)
调用栈关系:
javascript
function parentFunction() { // asyncId: 1
setTimeout(() => { // 创建时 init: asyncId=2, trigger=1
setTimeout(() => { // 创建时 init: asyncId=3, trigger=2
console.log('nested');
}, 100);
}, 100);
}
2. before(asyncId)
触发时机 :异步回调即将执行前(同步调用)
参数详解:
javascript
before(asyncId) // 即将执行的异步资源ID
执行顺序:
javascript
setTimeout(() => {
console.log('callback running');
}, 100);
// 100ms 后的执行顺序:
// 1. before(asyncId) ← 先调用
// 2. callback running ← 然后执行你的回调
// 3. after(asyncId) ← 最后调用
用途示例:
javascript
before(asyncId) {
// 这里可以设置上下文
currentContext = contextMap.get(asyncId);
console.log('即将执行异步回调,准备环境');
}
3. after(asyncId)
触发时机 :异步回调执行完毕后(同步调用)
参数详解:
javascript
after(asyncId) // 刚执行完的异步资源ID
重要特性:
- 即使回调抛出异常,
after依然会被调用 - 可以用来清理资源、恢复上下文
执行顺序示例:
javascript
setTimeout(() => {
console.log('1');
throw new Error('boom'); // 即使抛异常
console.log('2'); // 这行不会执行
}, 100);
// 钩子调用顺序:
// before(asyncId)
// → 1
// → after(asyncId) ← 依然会调用!
// → Error: boom
4. destroy(asyncId)
触发时机 :异步资源被销毁/清理时(可能延迟)
参数详解:
javascript
destroy(asyncId) // 被销毁的异步资源ID
触发场景:
javascript
// 场景1:setTimeout 执行完毕
const timer = setTimeout(() => {}, 100);
// 执行后自动销毁 → destroy(asyncId)
// 场景2:手动取消
const timer = setTimeout(() => {}, 10000);
clearTimeout(timer);
// 立即触发 → destroy(asyncId)
// 场景3:Promise 完成
Promise.resolve().then(() => {});
// 完成后销毁 → destroy(asyncId)
注意:destroy 可能不会立即调用,而是在垃圾回收时
二、所有 Type 类型详解
**用户常用类型(高频) **
为了篇幅去掉了一些,一共三十多种 .只展示了相对重要的。
1. Timeout
javascript
// 触发方式
setTimeout(callback, delay)
setInterval(callback, delay)
// resource 对象结构
resource._onTimeout // 回调函数
resource._idleTimeout // 延迟时间(毫秒)
resource._repeat // true=setInterval, false/undefined=setTimeout
// 生命周期
init → before → (回调执行) → after → destroy
2. Immediate
javascript
// 触发方式
setImmediate(callback)
// resource 对象结构
resource._onImmediate // 回调函数
// 特点:在 I/O 回调之后、定时器之前执行
3. PROMISE
javascript
// 触发方式
new Promise(executor)
Promise.resolve(value)
Promise.reject(error)
async function() {}
// 限制
resource 不包含 .then() 的回调函数
只能知道 Promise 被创建,无法获取回调内容
// 生命周期
init (创建) → before (resolve时) → after → destroy
4. TickObject
javascript
// 触发方式
process.nextTick(callback)
// 特点
- 在当前操作完成后立即执行
- 优先级高于 setImmediate
- 如果追踪会产生大量噪音
文件系统类型
5. FSREQCALLBACK
javascript
// 触发方式
fs.readFile(path, callback)
fs.writeFile(path, data, callback)
fs.stat(path, callback)
fs.open(path, flags, callback)
// resource 对象
包含内部文件操作信息,但不暴露回调函数
// 示例
fs.readFile('file.txt', (err, data) => {});
// → init(asyncId, 'FSREQCALLBACK', triggerId, resource)
6. FSEVENTWRAP
javascript
// 触发方式
fs.watch(path, callback)
fs.watchFile(path, callback)
// 用途:监听文件变化
7. STATWATCHER
javascript
// 触发方式
fs.watchFile(path, options, callback)
// 内部使用 fs.stat 轮询
进程相关类型
18. PROCESSWRAP
javascript
// 触发方式
const { spawn } = require('child_process');
spawn('ls', ['-la'])
// 代表:子进程的底层包装
19. PIPEWRAP
javascript
// 触发方式
自动创建于父子进程之间的通信管道
const child = spawn('node', ['child.js'], {
stdio: ['pipe', 'pipe', 'pipe']
});
信号与异步包装
20. SIGNALWRAP
javascript
// 触发方式
process.on('SIGINT', callback) // Ctrl+C
process.on('SIGTERM', callback) // kill 命令
process.on('SIGUSR1', callback) // 用户自定义信号
21. TIMERWRAP
javascript
// 内部类型
用于管理定时器的底层包装
不是你的 setTimeout,而是 libuv 的定时器
加密相关类型
22. PBKDF2REQUEST
javascript
// 触发方式
const crypto = require('crypto');
crypto.pbkdf2(password, salt, iterations, keylen, digest, callback)
// 密码派生函数(异步)
23. RANDOMBYTESREQUEST
javascript
// 触发方式
crypto.randomBytes(size, callback)
// 生成随机字节(异步)
24. TLSWRAP
javascript
// 触发方式
const tls = require('tls');
tls.connect(options, callback)
// TLS/SSL 加密套接字
其他类型
25. SHUTDOWNWRAP
javascript
// 触发方式
socket.end()
stream.destroy()
// 关闭流或套接字
26. WRITEWRAP
javascript
// 触发方式
stream.write(data, callback)
// 写入流时的异步包装
27. ZLIB
javascript
// 触发方式
const zlib = require('zlib');
zlib.gzip(buffer, callback)
zlib.deflate(buffer, callback)
// 压缩/解压缩操作
三、完整生命周期图示
plain
用户代码:setTimeout(() => console.log('hello'), 100);
↓
┌─────────────────────────────────────────────────┐
│ init(5, 'Timeout', 1, resource) │ ← 立即同步调用
│ - 创建异步资源 │
│ - 分配 asyncId = 5 │
│ - 记录触发者 triggerId = 1 │
└─────────────────────────────────────────────────┘
↓
... 等待 100ms ...
↓
┌─────────────────────────────────────────────────┐
│ before(5) │ ← 回调执行前
│ - 准备执行上下文 │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ console.log('hello') ← 你的回调执行 │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ after(5) │ ← 回调执行后
│ - 清理上下文 │
└─────────────────────────────────────────────────┘
↓
... 某个时刻(可能延迟) ...
↓
┌─────────────────────────────────────────────────┐
│ destroy(5) │ ← 资源销毁
│ - 释放资源 │
└─────────────────────────────────────────────────┘
四、重要特性总结
init 的特殊性
- 同步调用:在创建异步资源时立即执行
- 可能递归:如果你在 init 中创建新的异步操作,会再次触发 init
- 性能影响:每个异步操作都会触发,高频场景下开销大
before/after 的配对
- 总是成对出现(即使回调抛异常)
- 可以用来实现上下文传递(CLS - Continuation Local Storage)
- 用来测量异步回调的执行时间
destroy 的不确定性
- 可能延迟调用(依赖 GC)
- 某些类型可能不会触发 destroy
- 不能依赖 destroy 做关键的清理工作
asyncId 的唯一性
- 全局递增的数字
- 每个异步资源都有唯一的 asyncId
- 可以用来追踪异步调用链
五、使用建议
应该过滤的类型(噪音大)
javascript
const IGNORE_TYPES = [
'TTYWRAP', // console.log 会触发,导致递归
'TIMERWRAP', // 底层定时器管理,太频繁
'TickObject', // process.nextTick 太多
'PIPEWRAP', // 管道通信,底层实现
'SIGNALWRAP' // 信号处理,除非专门调试
];
应该追踪的类型(业务相关)
javascript
const TRACK_TYPES = [
'Timeout', // setTimeout/setInterval
'Immediate', // setImmediate
'PROMISE', // Promise/async-await
'FSREQCALLBACK', // 文件操作
'GETADDRINFOREQWRAP', // DNS 查询
'TCPCONNECTWRAP', // TCP 连接
'HTTPINCOMINGMESSAGE', // HTTP 请求/响应
'HTTPCLIENTREQUEST' // HTTP 客户端请求
];
Async Hooks实现
实现方式:写一个预加载脚本(--require),注册 hook 并输出调用栈。
javascript
// trace_async.js - Async Tracer
// Usage: node --require ./trace_async.js app.js
const async_hooks = require('async_hooks');
const fs = require('fs');
// Must use sync write to avoid recursion
function log(msg) {
fs.writeSync(process.stdout.fd, msg + '\n');
}
const asyncInfo = new Map();
// Filter types that cause recursion
const IGNORE_TYPES = [
'TTYWRAP', // Avoid console.log recursion
'TIMERWRAP',
'TickObject',
'SIGNALWRAP',
'PROCESSWRAP',
'PIPEWRAP',
'TCPWRAP',
'UDPWRAP',
'Immediate'
];
const hook = async_hooks.createHook({
init(asyncId, type, triggerAsyncId, resource) {
if (IGNORE_TYPES.includes(type)) {
return;
}
const stack = new Error().stack
.split('\n')
.slice(3, 6)
.map(line => line.trim())
.join('\n ');
asyncInfo.set(asyncId, {
type,
triggerAsyncId,
createdAt: Date.now()
});
if (type === 'Timeout') {
let callback = null;
let delay = null;
try {
callback = resource._onTimeout;
delay = resource._idleTimeout;
} catch (e) {}
const funcName = callback ? (callback.name || '(anonymous)') : 'unknown';
const funcSource = callback ? callback.toString() : 'N/A';
log(`
================================================================
[setTimeout CALLED]
================================================================
Async ID: ${asyncId}
Trigger ID: ${triggerAsyncId}
Delay: ${delay}ms
Callback Function: ${funcName}
Function Source Code:
${funcSource.split('\n').map(l => ' ' + l).join('\n')}
Call Stack:
${stack}
================================================================`);
} else if (type === 'PROMISE') {
log(`[Promise Created] id:${asyncId}, trigger:${triggerAsyncId}`);
} else if (type === 'FSREQCALLBACK') {
log(`[File Operation] id:${asyncId}, trigger:${triggerAsyncId}
Call Stack: ${stack}`);
} else {
log(`[${type}] id:${asyncId}, trigger:${triggerAsyncId}`);
}
},
before(asyncId) {
const info = asyncInfo.get(asyncId);
if (info && info.type === 'Timeout') {
log(`
----------------------------------------------------------
>> [setTimeout Callback START] id:${asyncId}
----------------------------------------------------------`);
}
},
after(asyncId) {
const info = asyncInfo.get(asyncId);
if (info && info.type === 'Timeout') {
const duration = Date.now() - info.createdAt;
log(`
----------------------------------------------------------
<< [setTimeout Callback END] id:${asyncId}
Total Duration: ${duration}ms
----------------------------------------------------------`);
}
},
destroy(asyncId) {
const info = asyncInfo.get(asyncId);
if (info && info.type === 'Timeout') {
log(`[setTimeout Resource Destroyed] id:${asyncId}`);
}
asyncInfo.delete(asyncId);
}
});
hook.enable();
log('\n========================================');
log('Async Hooks Tracer STARTED');
log('========================================\n');
process.on('beforeExit', () => {
hook.disable();
log('\n========================================');
log('Async Hooks Tracer STOPPED');
log('Undestroyed Resources: ' + asyncInfo.size);
log('========================================\n');
});
javascript
function aa(){
console.log('after 2s call aa() ')
}
console.log('begin ===')
setTimeout(aa,2000);
开始使用:
bash
PS D:\Code\htmlCode\my_sandbox_jsdom\testAndBak> node --require .\trace_async.js .\app.js
========================================
Async Hooks Tracer STARTED
========================================
begin ===
================================================================
[setTimeout CALLED]
================================================================
Async ID: 6
Trigger ID: 1
Delay: 2000ms
Callback Function: aa
Function Source Code:
function aa(){
console.log('after 2s call aa() ')
}
Call Stack:
at emitInitScript (node:internal/async_hooks:505:3)
at initAsyncResource (node:internal/timers:160:5)
at new Timeout (node:internal/timers:194:5)
================================================================
----------------------------------------------------------
----------------------------------------------------------
<< [setTimeout Callback END] id:6
Total Duration: 2013ms
----------------------------------------------------------
[setTimeout Resource Destroyed] id:6
========================================
Async Hooks Tracer STOPPED
Undestroyed Resources: 0
========================================
先介绍一下一点内容:
为什么要IGNORE_TYPES
这些是 Node.js 底层的异步资源类型,会产生大量噪音:
TTYWRAP - 终端输入输出
javascript
// 当你执行 console.log 时:
console.log('hello');
// ↓ 触发
// init(asyncId, 'TTYWRAP', ...) // 写入终端
// ↓ 如果你在 init 中又 console.log
// init(asyncId, 'TTYWRAP', ...) // 又触发
// ↓ 无限递归!💥
为什么忽略:避免日志输出本身触发新的异步操作导致死循环 ,
**这是解决了最大堆栈溢出的报错。 **
TIMERWRAP - 定时器底层包装
javascript
// 内部用于管理定时器的 C++ 对象
// 不是你的 setTimeout,而是 Node.js 内部的定时器管理器
为什么忽略 :太底层,你看到的 Timeout 才是你真正关心的
TickObject - process.nextTick 的内部实现
javascript
process.nextTick(() => {});
// 触发 TickObject
为什么忽略:太频繁,Node.js 内部大量使用,噪音很大
SIGNALWRAP - 信号处理
javascript
process.on('SIGINT', () => {}); // Ctrl+C
process.on('SIGTERM', () => {}); // kill 命令
为什么忽略:除非你在调试信号处理,否则不关心
PROCESSWRAP - 子进程包装
javascript
const { spawn } = require('child_process');
spawn('ls', ['-la']); // 内部使用 PROCESSWRAP
为什么忽略 :底层实现,你关心的是 ChildProcess 事件
PIPEWRAP / TCPWRAP / UDPWRAP - 网络/管道底层
javascript
// HTTP 请求、TCP 连接、管道通信的底层包装
const net = require('net');
net.createServer(); // 内部使用 TCPWRAP
为什么忽略 :太底层,你关心的是 http.request 或 net.Socket 事件
Immediate - setImmediate
javascript
setImmediate(() => {});
为什么忽略:如果你要追踪,可以保留;通常太频繁
更多文章,敬请关注gzh:零基础爬虫第一天
