理解 Node.js 的 Event loop

问题

考察如下代码,脑回路中运行并输出结果:

console.log("1");

setTimeout(function setTimeout1() {
  console.log("2");
  process.nextTick(function nextTick1() {
    console.log("3");
  });
  new Promise(function promise1(resolve) {
    console.log("4");
    resolve();
  }).then(function promiseThen1() {
    console.log("5");
  });
  setImmediate(function immediate1() {
    console.log("immediate");
  });
});

process.nextTick(function nextTick2() {
  console.log("6");
});

function bar() {
  console.log("bar");
}

async function foo() {
  console.log("async start");
  await bar();
  console.log("async end");
}

foo();

new Promise(function promise2(resolve) {
  console.log("7");
  resolve();
}).then(function promiseThen2() {
  console.log("8");
});

setTimeout(function setTimeout2() {
  console.log("9");

  new Promise(function promise3(resolve) {
    console.log("11");
    resolve();
  }).then(function promiseThen3() {
    console.log("12");
  });

  process.nextTick(function nextTick3() {
    console.log("10");
  });
});

JS 事件循环

JS 是单线程,朴素地讲,同时只能完成一件事件。如果有耗时的任务,那后续的所有任务都要等待其完成才能执行。

为了避免这种阻塞,引入了事件循环。即,将代码的执行分成一个个很小的阶段(一次循环),每个阶段重复相应的事情,直到所有任务都完成。

一个阶段包含以下部分:

  • Timers:到期的定时器任务,setTimeoutsetInterval 等注册的任务。
  • IO Callbacks:IO 操作,比如网络请求,文件读写。
  • IO Polling:IO 任务的注册
  • Set Immediate:通过 setImmediate 注册的任务
  • Close:close 事件的回调,比如 TCP 的断开。

Ticks and Phases of the Node.js Event Loop 图片来自 Daniel Khan 的 Medium 博客,见文末

同步代码及上面每个环节结束时都会清空一遍微任务队列,记住这点很重要!

代码执行流程

执行的流程是,

  • 将代码顺序执行。
  • 遇到异步任务,将任务压入待执行队列后继续往下。
  • 完成同步代码后,检查是否有微任务(通过 Promiseprocess.nextTickasync/await 等注册),如果有,则清空。
  • 清空微任务队列后,从待执行队列中取出最先压入的任务顺序执行,重复步骤一。

另,

  • async/await 本质上是 Promise,所以其表现会和 Promise 一致。
  • process.nextTick 注册的回调优先级高于定时器。
  • setImmediate 可看成 Node 版本的 setTimeout,所以可与后者同等对待。

示例代码分析

Round 1

  • 首先遇到同步代码 console.log(1),立即执行输出 1
  • 接下来是一个 setTimeout 定时器,将其回调压入待执行队列 [setTimeout1]
  • 遇到 process.nextTick,将其回调 nextTick2 压入微任务队列 [nextTick2]
  • 然后是 async 函数 foo 的调用,立即执行并输出 async start
  • 然后是 await 语句,这所在的地方会创建并返回 Promise,所以这里会执行其后面的表达式,也就是 bar() 函数的调用。
  • 执行 bar 函数,输出 bar
  • 在执行了 await 后面的语句后,它所代表的 Promise 就创建完成了,foo 函数体后续的代码相当于 promise 的 then,放入微任务队列 [nextTick2, rest_of_foo]
  • 继续往下遇到 new Promise,执行 Promise 的创建输出 7,将它的 then 回调压入微任务队列 [nextTick2, rest_of_foo,promiseThen2]
  • 遇到另一个 setTimeout,回调压入待执行队列 [setTimeout1,setTimeout2]
  • 至此,代码执行完了一轮。此时的输出应该是 1, async start, bar,7

Round 2

  • 查看微任务队列,并清空。所以依次执行 [nextTick2, rest_of_foo,promiseThen2],输出 6,async end,8

Round 3

  • 查看待执行队列 [setTimeout1,setTimeout2],先执行 setTimout1
  • 遇到 console.log(2) 输出2
  • 遇到 process.nextTicknextTick1 压入微任务队列 [nextTick1]
  • 遇到 new Promise 立即执行 输出 4,执行 resolve() 后将 promiseThen1 压入微任务队列 [nextTick1,promiseThen1]
  • 遇到 setImmediate 将回调压入待执行队列 [setTimeout2,immediate1]
  • 此时 setTimeout1 执行完毕,此时的输出应该为 2,4

Round 4

  • 检查微任务队列 [nextTick1,promiseThen1] 依次执行并输出 3,5

Round 5

  • 检查待执行队列 [setTimeout2,immediate1],执行 setTimeout2
  • 遇到 console输出 9
  • 遇到 new Promise 执行并输出 11,将 promiseThen3 压入微任务队列 [promiseThen3]
  • 遇到 process.nextTicknextTick3 压入微执行队列。注意,因为 process.nextTick 的优化级高于 Promise,所以压入后的结果是: [nextTick3,promiseThen3]
  • 此时 setTimeout2 执行完毕,输出为 9,11

Round 6

  • 检查微任务队列 [nextTick3,promiseThen3] 执行并输出 10,12

Round 7

  • 检查待执行队列 [immediate1],执行并输出 immediate

至此,走完了所有代码。

结果

以下是文章开头的结果:

1
async start
bar
7
6
async end
8
2
4
3
5
9
11
10
12
immediate

参考

原文地址:https://www.cnblogs.com/Wayou/p/understanding_event_loop.html

时间: 2024-08-03 09:01:56

理解 Node.js 的 Event loop的相关文章

方便大家学习的Node.js教程(一):理解Node.js

理解Node.js 为了理解Node.js是如何工作的,首先你需要理解一些使得Javascript适用于服务器端开发的关键特性.Javascript是一门简单而又灵活的语言,这种灵活性让它能够经受住时间的考验.函数.闭包等特性使Javascript成为一门适合Web开发的理想语言. 有一种偏见认为Javascript是不可靠的,然而事实并非如此.人们对Javascript的偏见来源于DOM,DOM是浏览器厂商提供的用于Javascript与浏览器交互的API,不同浏览器厂商实现的DOM存在差异.

理解Node.js的事件轮询

前言 总括 : 原文地址:理解Node.js的事件轮询 Node小应用:Node-sample 智者阅读群书.亦阅历人生 正文 Node.js的两个基本概念 Node.js的第一个基本概念就是I/O操作开销是巨大的: 所以,当前变成技术中最大的浪费来自于等待I/O操作的完毕.有几种方法能够解决性能的影响: 同步方式:按次序一个一个的处理请求.利:简单.弊:不论什么一个请求都能够堵塞其它全部请求. 开启新进程:每一个请求都开启一个新进程.利:简单:弊:大量的链接意味着大量的进程. 开启新线程:每一

理解 node.js 的事件循环

node.js 的第一个基本观点是,I/O 操作是昂贵的: 目前的编程技术最大的浪费来自等待 I/O 操作的完成.有几种方法可以解决这些对性能的影响(来自Sam Rushing): 同步:依次处理单个请求. 优点:简单. 缺点:任何一个请求都会阻塞其余请求. 创建新进程:为每个请求创建一个进程处理 优点:容易. 缺点:扩展性不好,数百个连接意味着数百个进程.fork()是 Unix 程序员的锤子.因为它很有用,所有的问题都像是钉子.但这通常是多余的. 线程:为每个请求创建一个线程处理. 优点:容

深入理解Javascript单线程谈Event Loop

假如面试回答js的运行机制时,你可能说出这么一段话:"Javascript的事件分同步任务和异步任务,遇到同步任务就放在执行栈中执行,而碰到异步任务就放到任务队列之中,等到执行栈执行完毕之后再去执行任务队列之中的事件."但你能说出背后的原因吗? 1.线程与进程 进程:是系统资源分配和调度的单元.一个运行着的程序就对应了一个进程.一个进程包括了运行中的程序和程序所使用到的内存和系统资源. 线程:线程是进程下的执行者,一个进程至少会开启一个线程(主线程),也可以开启多个线程. 2.同步和异

浅理解node.js

最近开始了解node.js,在这里把最近看的(七天学会nodejs)大概串一下,加深一下理解. node.js是一个解析器——一种运行环境,允许JS使用运行环境提供的内置对象和方法做一些事情.例如fs/http内置对象. ——运行在服务端的JavaScript 1 先来个实际点的小例子,感受一下nodejs var http = require('http'); http.createServer(function (request, response) { response.writeHead

【译】理解node.js事件轮询

Node.js的第一个基本论点是I/O开销很大. 当前编程技术中等待I/O完成会浪费大量的时间.有几种方法可以处理这种性能上的影响: 同步:每次处理一个请求,依次处理.优点:简单:缺点:任何一个请求都可以阻塞所有其他的请求. Fork一个新进程:开一个新进程来处理每个请求.优点:容易:缺点:不能很好的扩展,成百上千个连接意味着成百上千个进程.fork()函数相当于Unix程序员的锤子,因为它很有用,每个问题看起来就像一个钉子,通常会被过度使用.(译者注:直译比较拗口,我理解的意思是,Unix程序

理解Node.js安装及模块化

1.安装Node Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境. Node.js 使用了一个事件驱动.非阻塞式 I/O 的模型,使其轻量又高效. Node.js 的包管理器 npm(Node Package Manage),是全球最大的开源库生态系统. 中文网:http://nodejs.cn/英文网:https://nodejs.org 2.REPL(交互式解释器) REPL(Read Eval Print Loop:交互式解释器),表示一个电脑的环境

理解Node.js(译文)

正文 当我向别人介绍Node.js 的时候一般会有两种反应,要么是立马就弄明白它是个什么玩意儿,要么是被它搞的很糊涂. 如果你现在还处于后者,下面就是我对于node的解释: 它是一个命令行工具,你可以下载一个tarball文件,编译然后安装源文件: 它可以让你在你的终端输入node my_app.js来运行Javascript程序: Node的JS代码是由 V8 javascript 引擎(就是那个使得Chrome如此之快的东西)所执行的: Node提供了诸如访问网络或是操作文件系统的Javas

理解 Node.js 里的 process.nextTick()

有很多人对Node.js里process.nextTick()的用法感到不理解,下面我们就来看一下process.nextTick()到底是什么,该如何使用. Node.js是单线程的,除了系统IO之外,在它的事件轮询过程中,同一时间只会处理一个事件.你可以把事件轮询想象成一个大的队列,在每个时间点上,系统只会处理一个事件.即使你的电脑有多个CPU核心,你也无法同时并行的处理多个事件.但也就是这种特性使得node.js适合处理I/O型的应用,不适合那种CPU运算型的应用.在每个I/O型的应用中,