非同步鉤子#

穩定性:1 - 實驗性。如果可以,請遷移出此 API。我們不推薦使用 createHookAsyncHookexecutionAsyncResource API,因為它們存在可用性問題、安全風險和效能影響。非同步上下文跟蹤的用例最好由穩定的 AsyncLocalStorage API 來滿足。如果您有超出 AsyncLocalStorage 解決的上下文跟蹤需求或 診斷通道 當前提供的診斷資料之外的 createHookAsyncHookexecutionAsyncResource 的用例,請在 https://github.com/nodejs/node/issues 上提交一個 issue,描述您的用例,以便我們建立一個更具針對性的 API。

原始碼: lib/async_hooks.js

我們強烈不鼓勵使用 async_hooks API。其他可以覆蓋其大部分用例的 API 包括:

node:async_hooks 模組提供了一個用於跟蹤非同步資源的 API。可以使用以下方式訪問:

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

術語#

一個非同步資源表示一個具有關聯回撥的物件。這個回撥可能會被多次呼叫,例如 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(callbacks)#

註冊用於每個非同步操作的不同生命週期事件的回撥函式。

回撥函式 init()/before()/after()/destroy() 在資源的生命週期內,會為各自對應的非同步事件被呼叫。

所有回撥都是可選的。例如,如果只需要跟蹤資源清理,那麼只需要傳遞 destroy 回撥。所有可以傳遞給 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) { },
});

回撥將透過原型鏈繼承

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 是透過 async hooks 機制跟蹤其生命週期的非同步資源,所以 init()before()after()destroy() 回撥*不能*是返回 Promise 的非同步函式。

錯誤處理#

如果任何 AsyncHook 回撥丟擲異常,應用程式將列印堆疊跟蹤並退出。退出路徑遵循未捕獲異常的路徑,但所有 'uncaughtException' 監聽器都會被移除,從而強制程序退出。'exit' 回撥仍然會被呼叫,除非應用程式使用 --abort-on-uncaught-exception 執行,在這種情況下,將列印堆疊跟蹤並退出應用程式,留下一個核心檔案。

這種錯誤處理行為的原因是,這些回撥在物件生命週期中可能不穩定的點上執行,例如在類構造和析構期間。因此,為了防止將來發生意外中止,有必要迅速關閉程序。如果進行了全面的分析以確保異常可以遵循正常的控制流而沒有意外的副作用,這一點將來可能會改變。

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 例項。

鉤子回撥#

非同步事件生命週期中的關鍵事件被分為四個領域:例項化、回撥呼叫前後,以及例項銷燬時。

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 例項及其排程的非同步工作。

當使用公共嵌入器 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 中的鍵或向其新增屬性是不安全的。

非同步上下文示例#

上下文跟蹤用例由穩定的 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() 非同步呼叫。

一些資源的清理依賴於垃圾回收,所以如果對傳遞給 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() 返回的資源物件通常是內部的 Node.js 控制代碼物件,其 API 未被文件化。使用該物件上的任何函式或屬性都可能導致您的應用程式崩潰,應避免使用。

在頂級執行上下文中使用 executionAsyncResource() 將返回一個空物件,因為沒有控制代碼或請求物件可供使用,但擁有一個代表頂級的物件可能很有用。

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 上下文可能無法獲得精確的 executionAsyncIds。請參閱關於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#

  • 返回:一個從提供者型別到相應數字 ID 的對映。此對映包含可能由 async_hooks.init() 事件發出的所有事件型別。

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

Promise 執行跟蹤#

預設情況下,promise 的執行不會被分配 asyncId,因為 V8 提供的 promise 自省 API 相對昂貴。這意味著使用 promise 或 async/await 的程式預設情況下不會為 promise 回撥上下文獲取正確的執行和觸發 ID。

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

Promise.resolve(1729).then(() => {
  console.log(`eid ${executionAsyncId()} tid ${triggerAsyncId()}`);
});
// produces:
// eid 1 tid 0const { 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 6const { 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 的 asyncId6,後者的 asyncId7。在執行 then() 回撥期間,我們是在 asyncId7 的 promise 的上下文中執行。這個 promise 是由非同步資源 6 觸發的。

promise 的另一個微妙之處在於,beforeafter 回撥只在鏈式 promise 上執行。這意味著非由 then()/catch() 建立的 promise 不會觸發 beforeafter 回撥。更多細節請參見 V8 的 PromiseHooks API 的詳細資訊。

JavaScript 嵌入器 API#

處理自己的非同步資源(如 I/O、連線池或管理回撥佇列)的庫開發人員可以使用 AsyncResource JavaScript API,以便呼叫所有適當的回撥。

類: AsyncResource#

該類的文件已移至 AsyncResource

類: AsyncLocalStorage#

該類的文件已移至 AsyncLocalStorage