非同步鉤子 (Async hooks)#

穩定度:1 - 實驗性。如果可以,請從此 API 遷移。我們不建議使用 createHookAsyncHookexecutionAsyncResource API,因為它們存在易用性問題、安全風險和效能影響。非同步上下文追蹤的使用案例最好使用穩定的 AsyncLocalStorage API。如果您有除了 AsyncLocalStorage 解決的上下文追蹤需求,或目前由 Diagnostics Channel 提供的診斷數據之外,還有 createHookAsyncHookexecutionAsyncResource 的使用案例,請在 https://github.com/nodejs/node/issues 開啟一個議題並描述您的使用案例,以便我們建立一個更具針對性的 API。

我們強烈不建議使用 async_hooks API。其他可以涵蓋其大部分使用案例的 API 包括

node:async_hooks 模組提供了一個 API 來追蹤非同步資源。可以透過以下方式存取:

import async_hooks from 'node:async_hooks';
const async_hooks = require('node:async_hooks');

術語#

非同步資源代表一個帶有相關聯回呼 (callback) 的物件。此回呼可能會被呼叫多次,例如 net.createServer() 中的 'connection' 事件,或者只呼叫一次,例如 fs.open()。資源也可以在呼叫回呼之前關閉。AsyncHook 並未明確區分這些不同的情況,而是將它們表示為資源這個抽象概念。

如果使用了 Worker(工作執行緒),每個執行緒都有一個獨立的 async_hooks 介面,且每個執行緒都會使用一組新的非同步 ID。

總覽#

以下是公開 API 的簡單概述。

import async_hooks from 'node:async_hooks';

// Return the ID of the current execution context.
const eid = async_hooks.executionAsyncId();

// Return the ID of the handle responsible for triggering the callback of the
// current execution scope to call.
const tid = async_hooks.triggerAsyncId();

// Create a new AsyncHook instance. All of these callbacks are optional.
const asyncHook =
    async_hooks.createHook({ init, before, after, destroy, promiseResolve });

// Allow callbacks of this AsyncHook instance to call. This is not an implicit
// action after running the constructor, and must be explicitly run to begin
// executing callbacks.
asyncHook.enable();

// Disable listening for new asynchronous events.
asyncHook.disable();

//
// The following are the callbacks that can be passed to createHook().
//

// init() is called during object construction. The resource may not have
// completed construction when this callback runs. Therefore, all fields of the
// resource referenced by "asyncId" may not have been populated.
function init(asyncId, type, triggerAsyncId, resource) { }

// before() is called just before the resource's callback is called. It can be
// called 0-N times for handles (such as TCPWrap), and will be called exactly 1
// time for requests (such as FSReqCallback).
function before(asyncId) { }

// after() is called just after the resource's callback has finished.
function after(asyncId) { }

// destroy() is called when the resource is destroyed.
function destroy(asyncId) { }

// promiseResolve() is called only for promise resources, when the
// resolve() function passed to the Promise constructor is invoked
// (either directly or through other means of resolving a promise).
function promiseResolve(asyncId) { }
const async_hooks = require('node:async_hooks');

// Return the ID of the current execution context.
const eid = async_hooks.executionAsyncId();

// Return the ID of the handle responsible for triggering the callback of the
// current execution scope to call.
const tid = async_hooks.triggerAsyncId();

// Create a new AsyncHook instance. All of these callbacks are optional.
const asyncHook =
    async_hooks.createHook({ init, before, after, destroy, promiseResolve });

// Allow callbacks of this AsyncHook instance to call. This is not an implicit
// action after running the constructor, and must be explicitly run to begin
// executing callbacks.
asyncHook.enable();

// Disable listening for new asynchronous events.
asyncHook.disable();

//
// The following are the callbacks that can be passed to createHook().
//

// init() is called during object construction. The resource may not have
// completed construction when this callback runs. Therefore, all fields of the
// resource referenced by "asyncId" may not have been populated.
function init(asyncId, type, triggerAsyncId, resource) { }

// before() is called just before the resource's callback is called. It can be
// called 0-N times for handles (such as TCPWrap), and will be called exactly 1
// time for requests (such as FSReqCallback).
function before(asyncId) { }

// after() is called just after the resource's callback has finished.
function after(asyncId) { }

// destroy() is called when the resource is destroyed.
function destroy(asyncId) { }

// promiseResolve() is called only for promise resources, when the
// resolve() function passed to the Promise constructor is invoked
// (either directly or through other means of resolving a promise).
function promiseResolve(asyncId) { }

async_hooks.createHook(options)#

註冊要在每個非同步操作的不同生命週期事件中呼叫的函式。

回呼 init()/before()/after()/destroy() 會在資源生命週期中的各別非同步事件期間被呼叫。

所有回呼都是可選的。例如,如果只需要追蹤資源清理,則只需傳遞 destroy 回呼。所有可以傳遞給 callbacks 的函式的細節都在 鉤子回呼 (Hook Callbacks) 章節中。

import { createHook } from 'node:async_hooks';

const asyncHook = createHook({
  init(asyncId, type, triggerAsyncId, resource) { },
  destroy(asyncId) { },
});
const async_hooks = require('node:async_hooks');

const asyncHook = async_hooks.createHook({
  init(asyncId, type, triggerAsyncId, resource) { },
  destroy(asyncId) { },
});

回呼將透過原型鏈 (prototype chain) 繼承

class MyAsyncCallbacks {
  init(asyncId, type, triggerAsyncId, resource) { }
  destroy(asyncId) {}
}

class MyAddedCallbacks extends MyAsyncCallbacks {
  before(asyncId) { }
  after(asyncId) { }
}

const asyncHook = async_hooks.createHook(new MyAddedCallbacks());

由於 Promise 是非同步資源,其生命週期是透過非同步鉤子機制追蹤的,因此 init()before()after()destroy() 回呼 *絕對不能* 是回傳 Promise 的非同步函式 (async functions)。

錯誤處理#

如果任何 AsyncHook 回呼拋出錯誤,應用程式將列印堆疊追蹤並結束。結束路徑遵循未擷取例外 (uncaught exception) 的路徑,但所有 'uncaughtException' 接聽器都會被移除,從而強制程序結束。除非應用程式是以 --abort-on-uncaught-exception 執行,否則 'exit' 回呼仍會被呼叫,在該情況下會列印堆疊追蹤並結束應用程式,留下核心檔案 (core file)。

之所以採取這種錯誤處理行為,是因為這些回呼是在物件生命週期中可能不穩定的時間點執行的,例如在類別建構和銷毀期間。因此,為了防止未來發生非預期的中止,認為有必要迅速關閉程序。如果未來進行了全面分析以確保例外可以在不產生非預期副作用的情況下遵循正常控制流程,則這可能會有所改變。

AsyncHook 回呼中列印#

由於列印到主控台是非同步操作,console.log() 會導致 AsyncHook 回呼被呼叫。在 AsyncHook 回呼函式內部使用 console.log() 或類似的非同步操作會導致無限遞迴。除錯時一個簡單的解決方案是使用同步記錄操作,例如 fs.writeFileSync(file, msg, flag)。這將列印到檔案中,並且不會遞迴呼叫 AsyncHook,因為它是同步的。

import { writeFileSync } from 'node:fs';
import { format } from 'node:util';

function debug(...args) {
  // Use a function like this one when debugging inside an AsyncHook callback
  writeFileSync('log.out', `${format(...args)}\n`, { flag: 'a' });
}
const fs = require('node:fs');
const util = require('node:util');

function debug(...args) {
  // Use a function like this one when debugging inside an AsyncHook callback
  fs.writeFileSync('log.out', `${util.format(...args)}\n`, { flag: 'a' });
}

如果記錄需要非同步操作,可以使用 AsyncHook 本身提供的資訊來追蹤導致非同步操作的原因。當非同步操作是由記錄行為本身導致 AsyncHook 回呼被呼叫時,應跳過記錄。透過這樣做,可以打破原本的無限遞迴。

類別:AsyncHook#

AsyncHook 類別公開了一個用於追蹤非同步操作生命週期事件的介面。

asyncHook.enable()#

啟用給定 AsyncHook 執行體的回呼。如果未提供回呼,則啟用無效。

AsyncHook 執行體預設為停用。如果 AsyncHook 執行體在建立後應立即啟用,可以使用以下模式。

import { createHook } from 'node:async_hooks';

const hook = createHook(callbacks).enable();
const async_hooks = require('node:async_hooks');

const hook = async_hooks.createHook(callbacks).enable();

asyncHook.disable()#

從要執行的全域 AsyncHook 回呼池中停用給定 AsyncHook 執行體的回呼。一旦鉤子被停用,除非再次啟用,否則它將不會再被呼叫。

為了 API 的一致性,disable() 也會回傳 AsyncHook 執行體。

鉤子回呼 (Hook callbacks)#

非同步事件生命週期中的關鍵事件被分為四個領域:實體化、回呼被呼叫之前/之後,以及實體被銷毀時。

init(asyncId, type, triggerAsyncId, resource)#
  • asyncId <number> 非同步資源的唯一 ID。
  • type <string> 非同步資源的類型。
  • triggerAsyncId <number> 建立此非同步資源的執行上下文中,該非同步資源的唯一 ID。
  • resource <Object> 代表非同步操作的資源引用,需要在 destroy 期間釋放。

當建構一個 *有可能* 發出非同步事件的類別時呼叫。這 *並不* 代表實體在 destroy 被呼叫前必須呼叫 before/after,只是代表存在這種可能性。

這種行為可以透過執行類似開啟資源然後在資源使用前關閉它的操作來觀察。以下程式碼片段演示了這一點。

import { createServer } from 'node:net';

createServer().listen(function() { this.close(); });
// OR
clearTimeout(setTimeout(() => {}, 10));
require('node:net').createServer().listen(function() { this.close(); });
// OR
clearTimeout(setTimeout(() => {}, 10));

每個新資源都會被分配一個在目前 Node.js 執行體範圍內唯一的 ID。

type#

type 是一個字串,用於識別導致 init 被呼叫的資源類型。通常,它會對應資源建構函式的名稱。

由 Node.js 本身建立的資源 type 可能在任何 Node.js 版本中發生變化。有效值包括 TLSWRAPTCPWRAPTCPSERVERWRAPGETADDRINFOREQWRAPFSREQCALLBACKMicrotaskTimeout。請檢查所使用的 Node.js 版本的原始碼以獲取完整列表。

此外,AsyncResource 的使用者可以建立獨立於 Node.js 本身的非同步資源。

還有一種 PROMISE 資源類型,用於追蹤 Promise 實體及其排程的非同步工作。只有當 trackPromises 選項設定為 true 時,才會追蹤 Promise

使用者在使用公開的嵌入者 API 時可以定義自己的 type

類型名稱可能會發生衝突。建議嵌入者使用唯一的前綴(例如 npm 套件名稱),以防止在監聽鉤子時發生衝突。

triggerAsyncId#

triggerAsyncId 是導致(或「觸發」)新資源初始化並導致 init 呼叫的資源的 asyncId。這與 async_hooks.executionAsyncId() 不同,後者僅顯示資源建立的 *時間*,而 triggerAsyncId 顯示資源建立的 *原因*。

以下是 triggerAsyncId 的簡單演示

import { createHook, executionAsyncId } from 'node:async_hooks';
import { stdout } from 'node:process';
import net from 'node:net';
import fs from 'node:fs';

createHook({
  init(asyncId, type, triggerAsyncId) {
    const eid = executionAsyncId();
    fs.writeSync(
      stdout.fd,
      `${type}(${asyncId}): trigger: ${triggerAsyncId} execution: ${eid}\n`);
  },
}).enable();

net.createServer((conn) => {}).listen(8080);
const { createHook, executionAsyncId } = require('node:async_hooks');
const { stdout } = require('node:process');
const net = require('node:net');
const fs = require('node:fs');

createHook({
  init(asyncId, type, triggerAsyncId) {
    const eid = executionAsyncId();
    fs.writeSync(
      stdout.fd,
      `${type}(${asyncId}): trigger: ${triggerAsyncId} execution: ${eid}\n`);
  },
}).enable();

net.createServer((conn) => {}).listen(8080);

使用 nc localhost 8080 連接伺服器時的輸出

TCPSERVERWRAP(5): trigger: 1 execution: 1
TCPWRAP(7): trigger: 5 execution: 0

TCPSERVERWRAP 是接收連線的伺服器。

TCPWRAP 是來自用戶端的新連線。當建立新連線時,會立即建構 TCPWrap 實體。這發生在任何 JavaScript 堆疊之外。(executionAsyncId()0 表示它正從 C++ 執行,上方沒有 JavaScript 堆疊。)僅憑該資訊,將無法根據建立原因將資源連結在一起,因此 triggerAsyncId 的任務是傳播負責新資源存在的資源。

resource#

resource 是一個代表實際已初始化非同步資源的物件。存取該物件的 API 可能由資源的建立者指定。由 Node.js 本身建立的資源是內部的,可能隨時更改。因此,並未為這些資源指定 API。

在某些情況下,出於效能原因會重複使用資源物件,因此將其用作 WeakMap 中的鍵或為其新增屬性是不安全的。

非同步上下文 (Asynchronous context) 範例#

上下文追蹤的使用案例已由穩定的 API AsyncLocalStorage 涵蓋。此範例僅說明非同步鉤子的運作,但 AsyncLocalStorage 更適合此使用案例。

以下是一個範例,其中包含有關 beforeafter 呼叫之間 init 呼叫的額外資訊,特別是 listen() 的回呼看起來會是什麼樣子。輸出格式稍作設計,以便更容易看清呼叫上下文。

import async_hooks from 'node:async_hooks';
import fs from 'node:fs';
import net from 'node:net';
import { stdout } from 'node:process';
const { fd } = stdout;

let indent = 0;
async_hooks.createHook({
  init(asyncId, type, triggerAsyncId) {
    const eid = async_hooks.executionAsyncId();
    const indentStr = ' '.repeat(indent);
    fs.writeSync(
      fd,
      `${indentStr}${type}(${asyncId}):` +
      ` trigger: ${triggerAsyncId} execution: ${eid}\n`);
  },
  before(asyncId) {
    const indentStr = ' '.repeat(indent);
    fs.writeSync(fd, `${indentStr}before:  ${asyncId}\n`);
    indent += 2;
  },
  after(asyncId) {
    indent -= 2;
    const indentStr = ' '.repeat(indent);
    fs.writeSync(fd, `${indentStr}after:  ${asyncId}\n`);
  },
  destroy(asyncId) {
    const indentStr = ' '.repeat(indent);
    fs.writeSync(fd, `${indentStr}destroy:  ${asyncId}\n`);
  },
}).enable();

net.createServer(() => {}).listen(8080, () => {
  // Let's wait 10ms before logging the server started.
  setTimeout(() => {
    console.log('>>>', async_hooks.executionAsyncId());
  }, 10);
});
const async_hooks = require('node:async_hooks');
const fs = require('node:fs');
const net = require('node:net');
const { fd } = process.stdout;

let indent = 0;
async_hooks.createHook({
  init(asyncId, type, triggerAsyncId) {
    const eid = async_hooks.executionAsyncId();
    const indentStr = ' '.repeat(indent);
    fs.writeSync(
      fd,
      `${indentStr}${type}(${asyncId}):` +
      ` trigger: ${triggerAsyncId} execution: ${eid}\n`);
  },
  before(asyncId) {
    const indentStr = ' '.repeat(indent);
    fs.writeSync(fd, `${indentStr}before:  ${asyncId}\n`);
    indent += 2;
  },
  after(asyncId) {
    indent -= 2;
    const indentStr = ' '.repeat(indent);
    fs.writeSync(fd, `${indentStr}after:  ${asyncId}\n`);
  },
  destroy(asyncId) {
    const indentStr = ' '.repeat(indent);
    fs.writeSync(fd, `${indentStr}destroy:  ${asyncId}\n`);
  },
}).enable();

net.createServer(() => {}).listen(8080, () => {
  // Let's wait 10ms before logging the server started.
  setTimeout(() => {
    console.log('>>>', async_hooks.executionAsyncId());
  }, 10);
});

僅啟動伺服器時的輸出

TCPSERVERWRAP(5): trigger: 1 execution: 1
TickObject(6): trigger: 5 execution: 1
before:  6
  Timeout(7): trigger: 6 execution: 6
after:   6
destroy: 6
before:  7
>>> 7
  TickObject(8): trigger: 7 execution: 7
after:   7
before:  8
after:   8

如範例所示,executionAsyncId()execution 各自指定當前執行上下文的值;這是由 beforeafter 的呼叫所界定的。

僅使用 execution 來繪製資源分配圖會得到以下結果

  root(1)
     ^
     |
TickObject(6)
     ^
     |
 Timeout(7)

TCPSERVERWRAP 不是此圖表的一部分,儘管它是 console.log() 被呼叫的原因。這是因為繫結到不含主機名稱的埠號是一個 *同步* 操作,但為了保持完全非同步的 API,使用者的回呼被放在 process.nextTick() 中。這就是為什麼輸出中存在 TickObject 且它是 .listen() 回呼的「父級」。

該圖表僅顯示資源建立的 *時間*,而非 *原因*,因此要追蹤 *原因*,請使用 triggerAsyncId。可以用以下圖表表示

 bootstrap(1)
     |
     ˅
TCPSERVERWRAP(5)
     |
     ˅
 TickObject(6)
     |
     ˅
  Timeout(7)
before(asyncId)#

當發起非同步操作(例如 TCP 伺服器接收新連線)或完成非同步操作(例如將資料寫入磁碟)時,會呼叫回呼以通知使用者。before 回呼會在該回呼執行之前被呼叫。asyncId 是分配給即將執行回呼之資源的唯一識別碼。

before 回呼將被呼叫 0 到 N 次。如果非同步操作被取消,或者例如 TCP 伺服器未接收到任何連線,則 before 回呼通常會被呼叫 0 次。持久性的非同步資源(如 TCP 伺服器)通常會多次呼叫 before 回呼,而其他操作(如 fs.open())則只會呼叫一次。

after(asyncId)#

before 中指定的回呼完成後立即呼叫。

如果執行回呼期間發生未擷取的例外,則 after 將在 'uncaughtException' 事件發出或 domain 的處理常式執行 *之後* 執行。

destroy(asyncId)#

在對應 asyncId 的資源被銷毀後呼叫。它也從嵌入者 API emitDestroy() 中非同步呼叫。

某些資源依賴垃圾回收 (GC) 進行清理,因此如果對傳遞給 initresource 物件建立了引用,則 destroy 可能永遠不會被呼叫,從而導致應用程式記憶體洩漏。如果資源不依賴垃圾回收,則不會有此問題。

使用 destroy 鉤子會導致額外的開銷,因為它啟用了透過垃圾回收器對 Promise 實體的追蹤。

promiseResolve(asyncId)#

當傳遞給 Promise 建構函式的 resolve 函式被叫用時(無論是直接叫用還是透過其他解析 Promise 的方式)呼叫。

resolve() 不會執行任何可觀察到的同步工作。

如果 Promise 是透過採用另一個 Promise 的狀態來解析的,此時 Promise 不一定已完成 (fulfilled) 或被拒絕 (rejected)。

new Promise((resolve) => resolve(true)).then((a) => {});

呼叫以下回呼

init for PROMISE with id 5, trigger id: 1
  promise resolve 5      # corresponds to resolve(true)
init for PROMISE with id 6, trigger id: 5  # the Promise returned by then()
  before 6               # the then() callback is entered
  promise resolve 6      # the then() callback resolves the promise by returning
  after 6

async_hooks.executionAsyncResource()#

  • 回傳:<Object> 代表目前執行的資源。用於在資源中儲存資料。

executionAsyncResource() 回傳的資源物件通常是帶有未記載 API 的內部 Node.js Handle 物件。在物件上使用任何函式或屬性都可能導致您的應用程式崩潰,應予以避免。

在頂層執行上下文中使用 executionAsyncResource() 將回傳一個空物件,因為沒有可使用的 Handle 或 Request 物件,但擁有一個代表頂層的物件可能會有幫助。

import { open } from 'node:fs';
import { executionAsyncId, executionAsyncResource } from 'node:async_hooks';

console.log(executionAsyncId(), executionAsyncResource());  // 1 {}
open(new URL(import.meta.url), 'r', (err, fd) => {
  console.log(executionAsyncId(), executionAsyncResource());  // 7 FSReqWrap
});
const { open } = require('node:fs');
const { executionAsyncId, executionAsyncResource } = require('node:async_hooks');

console.log(executionAsyncId(), executionAsyncResource());  // 1 {}
open(__filename, 'r', (err, fd) => {
  console.log(executionAsyncId(), executionAsyncResource());  // 7 FSReqWrap
});

這可以用於實現接續區域儲存 (continuation local storage),而無需使用追蹤用的 Map 來儲存元數據

import { createServer } from 'node:http';
import {
  executionAsyncId,
  executionAsyncResource,
  createHook,
} from 'node:async_hooks';
const sym = Symbol('state'); // Private symbol to avoid pollution

createHook({
  init(asyncId, type, triggerAsyncId, resource) {
    const cr = executionAsyncResource();
    if (cr) {
      resource[sym] = cr[sym];
    }
  },
}).enable();

const server = createServer((req, res) => {
  executionAsyncResource()[sym] = { state: req.url };
  setTimeout(function() {
    res.end(JSON.stringify(executionAsyncResource()[sym]));
  }, 100);
}).listen(3000);
const { createServer } = require('node:http');
const {
  executionAsyncId,
  executionAsyncResource,
  createHook,
} = require('node:async_hooks');
const sym = Symbol('state'); // Private symbol to avoid pollution

createHook({
  init(asyncId, type, triggerAsyncId, resource) {
    const cr = executionAsyncResource();
    if (cr) {
      resource[sym] = cr[sym];
    }
  },
}).enable();

const server = createServer((req, res) => {
  executionAsyncResource()[sym] = { state: req.url };
  setTimeout(function() {
    res.end(JSON.stringify(executionAsyncResource()[sym]));
  }, 100);
}).listen(3000);

async_hooks.executionAsyncId()#

  • 回傳:<number> 目前執行上下文的 asyncId。用於追蹤何時發生呼叫。
import { executionAsyncId } from 'node:async_hooks';
import fs from 'node:fs';

console.log(executionAsyncId());  // 1 - bootstrap
const path = '.';
fs.open(path, 'r', (err, fd) => {
  console.log(executionAsyncId());  // 6 - open()
});
const async_hooks = require('node:async_hooks');
const fs = require('node:fs');

console.log(async_hooks.executionAsyncId());  // 1 - bootstrap
const path = '.';
fs.open(path, 'r', (err, fd) => {
  console.log(async_hooks.executionAsyncId());  // 6 - open()
});

executionAsyncId() 回傳的 ID 與執行時機有關,與因果關係無關(因果關係由 triggerAsyncId() 涵蓋)

const server = net.createServer((conn) => {
  // Returns the ID of the server, not of the new connection, because the
  // callback runs in the execution scope of the server's MakeCallback().
  async_hooks.executionAsyncId();

}).listen(port, () => {
  // Returns the ID of a TickObject (process.nextTick()) because all
  // callbacks passed to .listen() are wrapped in a nextTick().
  async_hooks.executionAsyncId();
});

預設情況下,Promise 上下文可能無法獲得精確的 executionAsyncId。請參閱 Promise 執行追蹤 章節。

async_hooks.triggerAsyncId()#

  • 回傳:<number> 負責呼叫目前正在執行之回呼的資源 ID。
const server = net.createServer((conn) => {
  // The resource that caused (or triggered) this callback to be called
  // was that of the new connection. Thus the return value of triggerAsyncId()
  // is the asyncId of "conn".
  async_hooks.triggerAsyncId();

}).listen(port, () => {
  // Even though all callbacks passed to .listen() are wrapped in a nextTick()
  // the callback itself exists because the call to the server's .listen()
  // was made. So the return value would be the ID of the server.
  async_hooks.triggerAsyncId();
});

預設情況下,Promise 上下文可能無法獲得有效的 triggerAsyncId。請參閱 Promise 執行追蹤 章節。

async_hooks.asyncWrapProviders#

  • 回傳:一個供應商類型 (provider types) 到相應數字 ID 的對照表。此對照表包含所有可能由 async_hooks.init() 事件發出的事件類型。

此功能取代了已棄用的 process.binding('async_wrap').Providers 用法。參見:DEP0111

Promise 執行追蹤#

預設情況下,由於 V8 提供的 Promise 內省 API (promise introspection API) 成本相對昂貴,因此 Promise 執行不會被分配 asyncId。這代表使用 Promise 或 async/await 的程式預設不會為 Promise 回呼上下文獲得正確的執行 ID 和觸發 ID。

import { executionAsyncId, triggerAsyncId } from 'node:async_hooks';

Promise.resolve(1729).then(() => {
  console.log(`eid ${executionAsyncId()} tid ${triggerAsyncId()}`);
});
// produces:
// eid 1 tid 0
const { executionAsyncId, triggerAsyncId } = require('node:async_hooks');

Promise.resolve(1729).then(() => {
  console.log(`eid ${executionAsyncId()} tid ${triggerAsyncId()}`);
});
// produces:
// eid 1 tid 0

觀察到 then() 回呼聲稱是在外部範圍的上下文中執行的,儘管涉及到了非同步跳轉。此外,triggerAsyncId 的值為 0,這表示我們缺少關於導致(觸發)then() 回呼執行之資源的上下文。

透過 async_hooks.createHook 安裝非同步鉤子可啟用 Promise 執行追蹤

import { createHook, executionAsyncId, triggerAsyncId } from 'node:async_hooks';
createHook({ init() {} }).enable(); // forces PromiseHooks to be enabled.
Promise.resolve(1729).then(() => {
  console.log(`eid ${executionAsyncId()} tid ${triggerAsyncId()}`);
});
// produces:
// eid 7 tid 6
const { createHook, executionAsyncId, triggerAsyncId } = require('node:async_hooks');

createHook({ init() {} }).enable(); // forces PromiseHooks to be enabled.
Promise.resolve(1729).then(() => {
  console.log(`eid ${executionAsyncId()} tid ${triggerAsyncId()}`);
});
// produces:
// eid 7 tid 6

在此範例中,新增任何實際的鉤子函式都會啟用 Promise 的追蹤。上述範例中有兩個 Promise:由 Promise.resolve() 建立的 Promise 以及由 then() 呼叫回傳的 Promise。在上述範例中,第一個 Promise 獲得了 asyncId 6,後者獲得了 asyncId 7。在 then() 回呼執行期間,我們是在 asyncId7 的 Promise 上下文中執行的。此 Promise 是由非同步資源 6 觸發的。

Promise 的另一個微妙之處是 beforeafter 回呼僅在鏈式 Promise 上執行。這代表不是由 then()/catch() 建立的 Promise 將不會觸發 beforeafter 回呼。有關更多詳細資訊,請參閱 V8 PromiseHooks API 的細節。

停用 Promise 執行追蹤#

追蹤 Promise 執行可能會造成顯著的效能開銷。要取消 Promise 追蹤,請將 trackPromises 設定為 false

const { createHook } = require('node:async_hooks');
const { writeSync } = require('node:fs');
createHook({
  init(asyncId, type, triggerAsyncId, resource) {
    // This init hook does not get called when trackPromises is set to false.
    writeSync(1, `init hook triggered for ${type}\n`);
  },
  trackPromises: false,  // Do not track promises.
}).enable();
Promise.resolve(1729);
import { createHook } from 'node:async_hooks';
import { writeSync } from 'node:fs';

createHook({
  init(asyncId, type, triggerAsyncId, resource) {
    // This init hook does not get called when trackPromises is set to false.
    writeSync(1, `init hook triggered for ${type}\n`);
  },
  trackPromises: false,  // Do not track promises.
}).enable();
Promise.resolve(1729);

JavaScript 嵌入者 (Embedder) API#

處理其自身非同步資源(執行 I/O、連線池或管理回呼佇列等任務)的函式庫開發人員可以使用 AsyncResource JavaScript API,以便呼叫所有適當的回呼。

類別:AsyncResource#

此類別的說明文件已移至 AsyncResource

類別:AsyncLocalStorage#

此類別的說明文件已移至 AsyncLocalStorage