您可能已经注意到 process.nextTick() 没有显示在图中,即使它是异步API的一部分。 这是因为 process.nextTick() 在技术上不是事件循环的一部分。 相反, nextTickQueue 将在当前操作完成后处理,而不管事件循环的当前阶段如何。
回顾一下我们的图表,无论何时在给定阶段调用 process.nextTick() ,传递给 process.nextTick() 的所有回调都将在事件循环继续之前得到解决。 这可能会产生一些不好的情况, 因为它允许您通过进行递归的 process.nextTick() 调用来“饿死”您的I / O, 这会阻止事件循环到达轮询阶段。
为什么会被允许?
为什么这样的东西会被包含在Node.js中? 其中一部分是一种设计理念,其中API应该始终是异步的,即使它不是必须的。 以此代码段为例:
function apiCall(arg, callback) { if (typeof arg !== 'string') return process.nextTick(callback, new TypeError('argument should be string')); }
这段代码进行参数检查,如果不正确,它会将错误传递给回调。 最近更新的API允许将参数传递给 process.nextTick() ,允许它将回调后传递的任何参数作为参数传播到回调,因此您不必嵌套函数。
我们正在做的是将错误传回给用户,但只有在我们允许其余的用户代码执行之后。 通过使用 process.nextTick() ,我们保证 apiCall() 始终在用户代码的其余部分之后和允许事件循环继续之前运行其回调。 为了实现这一点,允许JS调用堆栈展开然后立即执行提供的回调,这允许一个人对 process.nextTick() 进行递归调用而不会达到 RangeError :超出v8的最大调用堆栈大小。
这种理念可能会导致一些潜在的问题。 以此片段为例:
let bar; // this has an asynchronous signature, but calls callback synchronously function someAsyncApiCall(callback) { callback(); } // the callback is called before `someAsyncApiCall` completes. someAsyncApiCall(() => { // since someAsyncApiCall has completed, bar hasn't been assigned any value console.log('bar', bar); // undefined }); bar = 1;
用户将 someAsyncApiCall() 定义为具有异步签名,但它实际上是同步操作的。 调用它时,在事件循环的同一阶段调用提供给 someAsyncApiCall() 的回调,因为 someAsyncApiCall() 实际上不会异步执行任何操作。 因此,回调尝试引用bar,即使它在范围内可能没有该变量,因为该脚本无法运行完成。
通过将回调放在 process.nextTick() 中,脚本仍然能够运行完成,允许在调用回调之前初始化所有变量,函数等。 它还具有不允许事件循环继续的优点。 在允许事件循环继续之前,向用户警告错误可能是有用的。 以下是使用 process.nextTick() 的前一个示例:
let bar; function someAsyncApiCall(callback) { process.nextTick(callback); } someAsyncApiCall(() => { console.log('bar', bar); // 1 }); bar = 1;
这是另一个真实世界的例子:
const server = net.createServer(() => {}).listen(8080); server.on('listening', () => {});
仅传递端口时,端口立即绑定。 因此,可以立即调用 'listening' 回调。 问题是那时候不会设置 .on('listening') 回调。
为了解决这个问题, 'listening' 事件在 nextTick() 中排队,以允许脚本运行完成。 这允许用户设置他们想要的任何事件处理程序。
process.nextTick() vs setImmediate()
就用户而言,我们有两个类似的调用,但它们的名称令人困惑。
process.nextTick() setImmediate()
实质上,应该交换名称。 process.nextTick() 比 setImmediate() 更快地触发,但这是过去创造的,不太可能改变。 进行此切换会破坏npm上的大部分包。 每天都会添加更多新模块,这意味着我们每天都在等待,更多的潜在破损发生。 虽然它们令人困惑,但自身的叫法不会改变。
我们建议开发人员在所有情况下都使用 setImmediate() ,因为它更容易推理(并且它导致代码与更广泛的环境兼容,如浏览器JS。)
为什么要使用 process.nextTick() ?
有两个主要原因:
允许用户处理错误,清除任何不需要的资源,或者在事件循环继续之前再次尝试请求。
有时需要允许回调在调用堆栈展开之后但在事件循环继续之前运行。
一个例子是匹配用户的期望。 简单的例子:
const server = net.createServer(); server.on('connection', (conn) => { }); server.listen(8080); server.on('listening', () => { });
假设 listen() 在事件循环开始时运行,但是监听回调放在 setImmediate() 中。 除非传递主机名,否则将立即绑定到端口。 要使事件循环继续,它必须达到轮询阶段,这意味着可能已经接收到连接的非零概率允许在侦听事件之前触发连接事件。
另一个例子是运行一个函数构造函数,比如继承自 EventEmitter ,它想在构造函数中调用一个事件: