本篇文章给大家了解一下Node.js中的事件循环,解析下事件循环机制、process.nextTick()等,希望对大家有所帮助!
本篇文章给大家了解一下Nodejs中的事件循环,解析下事件循环机制、process.nextTick()等,希望对大家有所帮助!什么是事件循环事件循环是 Node.js 处理非阻塞 I/O 操作的机制——尽管 JavaScript 是单线程处理的——当有可能的时候,它们会把操作转移到系统内核中去。 既然目前大多数内核都是多线程的,它们可在后台处理多种操作。当其中的一个操作完成的时候,内核通知 Node.js 将适合的回调函数添加到轮询队列中等待时机执行。我们在本文后面会进行详细介绍。 事件循环机制解析当 Node.js 启动后,它会初始化事件循环,处理已提供的输入脚本(或丢入 REPL,本文不涉及到),它可能会调用一些异步的 API、调度定时器,或者调用 下面的图表展示了事件循环操作顺序的简化概览。 ┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ │ └─────────────┬─────────────┘ ┌───────────────┐ │ ┌─────────────┴─────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └─────────────┬─────────────┘ │ data, etc. │ │ ┌─────────────┴─────────────┐ └───────────────┘ │ │ check │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ └───────────────────────────┘ 注意:每个框被称为事件循环机制的一个阶段。 每个阶段都有一个 FIFO 队列来执行回调。虽然每个阶段都是特殊的,但通常情况下,当事件循环进入给定的阶段时,它将执行特定于该阶段的任何操作,然后执行该阶段队列中的回调,直到队列用尽或最大回调数已执行。当该队列已用尽或达到回调限制,事件循环将移动到下一阶段,等等。 由于这些操作中的任何一个都可能调度_更多的_操作和由内核排列在轮询阶段被处理的新事件, 且在处理轮询中的事件时,轮询事件可以排队。因此,长时间运行的回调可以允许轮询阶段运行长于计时器的阈值时间。有关详细信息,请参阅 计时器 和 轮询 部分。 注意: 在 Windows 和 Unix/Linux 实现之间存在细微的差异,但这对演示来说并不重要。最重要的部分在这里。实际上有七或八个步骤,但我们关心的是 Node.js 实际上使用以上的某些步骤。 阶段概述
在每次运行的事件循环之间,Node.js 检查它是否在等待任何异步 I/O 或计时器,如果没有的话,则完全关闭。 阶段的详细概述定时器计时器指定可以执行所提供回调的 阈值,而不是用户希望其执行的确切时间。在指定的一段时间间隔后, 计时器回调将被尽可能早地运行。但是,操作系统调度或其它正在运行的回调可能会延迟它们。 注意:轮询 阶段 控制何时定时器执行。 例如,假设您调度了一个在 100 毫秒后超时的定时器,然后您的脚本开始异步读取会耗费 95 毫秒的文件: const fs = require('fs'); function someAsyncOperation(callback) { // Assume this takes 95ms to complete fs.readFile('/path/to/file', callback); } const timeoutScheduled = Date.now(); setTimeout(() => { const delay = Date.now() - timeoutScheduled; console.log(`${delay}ms have passed since I was scheduled`); }, 100); // do someAsyncOperation which takes 95 ms to complete someAsyncOperation(() => { const startCallback = Date.now(); // do something that will take 10ms... while (Date.now() - startCallback < 10) { // do nothing } }); 当事件循环进入 轮询 阶段时,它有一个空队列(此时 注意:为了防止 轮询 阶段饿死事件循环,libuv(实现 Node.js 事件循环和平台的所有异步行为的 C 函数库),在停止轮询以获得更多事件之前,还有一个硬性最大值(依赖于系统)。 挂起的回调函数此阶段对某些系统操作(如 TCP 错误类型)执行回调。例如,如果 TCP 套接字在尝试连接时接收到 轮询轮询 阶段有两个重要的功能:
当事件循环进入 轮询 阶段且_没有被调度的计时器时_,将发生以下两种情况之一:
一旦 轮询 队列为空,事件循环将检查 _已达到时间阈值的计时器_。如果一个或多个计时器已准备就绪,则事件循环将绕回计时器阶段以执行这些计时器的回调。 检查阶段此阶段允许人员在轮询阶段完成后立即执行回调。如果轮询阶段变为空闲状态,并且脚本使用
通常,在执行代码时,事件循环最终会命中轮询阶段,在那等待传入连接、请求等。但是,如果回调已使用 关闭的回调函数如果套接字或处理函数突然关闭(例如 setImmediate() 对比 setTimeout()
执行计时器的顺序将根据调用它们的上下文而异。如果二者都从主模块内调用,则计时器将受进程性能的约束(这可能会受到计算机上其他正在运行应用程序的影响)。 例如,如果运行以下不在 I/O 周期(即主模块)内的脚本,则执行两个计时器的顺序是非确定性的,因为它受进程性能的约束: // timeout_vs_immediate.js setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); $ node timeout_vs_immediate.js timeout immediate $ node timeout_vs_immediate.js immediate timeout 但是,如果你把这两个函数放入一个 I/O 循环内调用,setImmediate 总是被优先调用: // timeout_vs_immediate.js const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); }); $ node timeout_vs_immediate.js immediate timeout $ node timeout_vs_immediate.js immediate timeout 使用 process.nextTick()理解 process.nextTick()您可能已经注意到 回顾我们的图示,任何时候在给定的阶段中调用 为什么会允许这样?为什么这样的事情会包含在 Node.js 中?它的一部分是一个设计理念,其中 API 应该始终是异步的,即使它不必是。以此代码段为例: function apiCall(arg, callback) { if (typeof arg !== 'string') return process.nextTick( callback, new TypeError('argument should be string') ); } 代码段进行参数检查。如果不正确,则会将错误传递给回调函数。最近对 API 进行了更新,允许传递参数给 我们正在做的是将错误传回给用户,但仅在执行用户的其余代码之后。通过使用 这种设计原理可能会导致一些潜在的问题。 以此代码段为例: 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; 用户将 通过将回调置于 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', () => {}); 只有传递端口时,端口才会立即被绑定。因此,可以立即调用 为了绕过这个问题, process.nextTick() 对比 setImmediate()就用户而言,我们有两个类似的调用,但它们的名称令人费解。
实质上,这两个名称应该交换,因为 我们建议开发人员在所有情况下都使用 为什么要使用 process.nextTick()?有两个主要原因:
以下是一个符合用户预期的简单示例: const server = net.createServer(); server.on('connection', (conn) => {}); server.listen(8080); server.on('listening', () => {}); 假设 另一个示例运行的函数构造函数是从 const EventEmitter = require('events'); const util = require('util'); function MyEmitter() { EventEmitter.call(this); this.emit('event'); } util.inherits(MyEmitter, EventEmitter); const myEmitter = new MyEmitter(); myEmitter.on('event', () => { console.log('an event occurred!'); }); 你不能立即从构造函数中触发事件,因为脚本尚未处理到用户为该事件分配回调函数的地方。因此,在构造函数本身中可以使用 const EventEmitter = require('events'); const util = require('util'); function MyEmitter() { EventEmitter.call(this); // use nextTick to emit the event once a handler is assigned process.nextTick(() => { this.emit('event'); }); } util.inherits(MyEmitter, EventEmitter); const myEmitter = new MyEmitter(); myEmitter.on('event', () => { console.log('an event occurred!'); });
更多node相关知识,请访问:nodejs 教程! 以上就是了解Node中的事件循环、process.nextTick()的详细内容,更多请关注模板之家(www.mb5.com.cn)其它相关文章! |