【node源码-5】Async Hooks使用

这个可以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.requestnet.Socket 事件

Immediate - setImmediate

javascript 复制代码
setImmediate(() => {});

为什么忽略:如果你要追踪,可以保留;通常太频繁


更多文章,敬请关注gzh:零基础爬虫第一天

相关推荐
傻啦嘿哟7 小时前
Python爬虫进阶:反爬机制突破与数据存储实战指南
开发语言·爬虫·python
sugar椰子皮8 小时前
【node源码-2】Node.js 启动流程
爬虫·node.js
不会飞的鲨鱼8 小时前
抖音验证码滑动轨迹原理(续)
javascript·爬虫·python
失败又激情的man10 小时前
爬虫逆向之阿里系cookie acw_sc__v2 逆向分析
前端·javascript·爬虫
盼哥PyAI实验室11 小时前
Python 爬虫核心基础:请求与响应机制全解析(从 GET 请求到 JSON 分页实战)
爬虫·python·json
电商API_180079052471 天前
淘宝评论API技术解析与调用实战指南
开发语言·爬虫·信息可视化
嫂子的姐夫1 天前
008-字体反爬:猫眼
爬虫·逆向·混淆·字体反爬
APIshop1 天前
用第三方爬虫调用「淘宝评论 API」全流程实战
开发语言·爬虫
是有头发的程序猿1 天前
Python爬虫实战:面向对象编程构建高可维护的1688商品数据采集系统
开发语言·爬虫·python