与异步任务和 Promise 的超时交互


Promiseasync function 可以异步地调度 JavaScript 引擎运行的任务。 默认情况下,这些任务在当前堆栈上的所有 JavaScript 函数执行完毕后运行。 这允许转义 timeoutbreakOnSigint 选项的功能。

例如,以下代码由 vm.runInNewContext() 执行,超时时间为 5 毫秒,它安排了一个无限循环在 promise 解决后运行。 计划的循环永远不会被超时中断:

const vm = require('node:vm');

function loop() {
  console.log('entering loop');
  while (1) console.log(Date.now());
}

vm.runInNewContext(
  'Promise.resolve().then(() => loop());',
  { loop, console },
  { timeout: 5 },
);
// 这是在 'entering loop' 之前打印 (!)
console.log('done executing');

这可以通过将 microtaskMode: 'afterEvaluate' 传给创建 Context 的代码来解决:

const vm = require('node:vm');

function loop() {
  while (1) console.log(Date.now());
}

vm.runInNewContext(
  'Promise.resolve().then(() => loop());',
  { loop, console },
  { timeout: 5, microtaskMode: 'afterEvaluate' },
);

在这种情况下,通过 promise.then() 调度的微任务将在从 vm.runInNewContext() 返回之前运行,并会被 timeout 功能中断。 这仅适用于在 vm.Context 中运行的代码,因此例如 vm.runInThisContext() 不采用此选项。

Promise 回调被输入到创建它们的上下文的微任务队列中。 例如,如果在上面的例子中 () => loop() 只被 loop 替换,那么 loop 将被推入全局微任务队列,因为它是来自外部(主)上下文的函数,因此也将能够逃脱超时。

process.nextTick()queueMicrotask()setTimeout()setImmediate() 等异步调度函数。 在 vm.Context 中可用,传给它们的函数将被添加到全局队列中,由所有上下文共享。 因此,传给这些函数的回调也无法通过超时控制。

Promises and async functions can schedule tasks run by the JavaScript engine asynchronously. By default, these tasks are run after all JavaScript functions on the current stack are done executing. This allows escaping the functionality of the timeout and breakOnSigint options.

For example, the following code executed by vm.runInNewContext() with a timeout of 5 milliseconds schedules an infinite loop to run after a promise resolves. The scheduled loop is never interrupted by the timeout:

const vm = require('node:vm');

function loop() {
  console.log('entering loop');
  while (1) console.log(Date.now());
}

vm.runInNewContext(
  'Promise.resolve().then(() => loop());',
  { loop, console },
  { timeout: 5 },
);
// This is printed *before* 'entering loop' (!)
console.log('done executing');

This can be addressed by passing microtaskMode: 'afterEvaluate' to the code that creates the Context:

const vm = require('node:vm');

function loop() {
  while (1) console.log(Date.now());
}

vm.runInNewContext(
  'Promise.resolve().then(() => loop());',
  { loop, console },
  { timeout: 5, microtaskMode: 'afterEvaluate' },
);

In this case, the microtask scheduled through promise.then() will be run before returning from vm.runInNewContext(), and will be interrupted by the timeout functionality. This applies only to code running in a vm.Context, so e.g. vm.runInThisContext() does not take this option.

Promise callbacks are entered into the microtask queue of the context in which they were created. For example, if () => loop() is replaced with just loop in the above example, then loop will be pushed into the global microtask queue, because it is a function from the outer (main) context, and thus will also be able to escape the timeout.

If asynchronous scheduling functions such as process.nextTick(), queueMicrotask(), setTimeout(), setImmediate(), etc. are made available inside a vm.Context, functions passed to them will be added to global queues, which are shared by all contexts. Therefore, callbacks passed to those functions are not controllable through the timeout either.