问题

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

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

参考

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

  1. 浅析Node.js的Event Loop

    目录 浅析Node.js的Event Loop 引出问题 Node.js的基本架构 Libuv Event Loop Event Loop Phases Overview Poll Phase The ...

  2. 定时器setTimeout()和Node.js的Event Loop

    一.定时器 setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行.它在"任务队列"的尾部添加一个事件,因此要等到同步任务和 ...

  3. 【Node.js】Event Loop执行顺序详解

    本文基于node 0.10.22版本 关于EventLoop是什么,请看阮老师写的什么是EventLoop 本文讲述的是EventLoop中的执行顺序(着重讲setImmediate, setTime ...

  4. Node.js学习 - Event Loop

    Node.js本身是单线程,但通过事件和回调支持并发,所以性能非常高. Node.js的每一个API都是异步的,并作为一个独立线程运行,使用异步函数调用,并处理并发. 事件驱动程序 实例 var ev ...

  5. 理解Node.js的事件轮询

    前言 总括 : 原文地址:理解Node.js的事件轮询 Node小应用:Node-sample 智者阅读群书,亦阅历人生 正文 Node.js的两个基本概念 Node.js的第一个基本概念就是I/O操 ...

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

    理解Node.js 为了理解Node.js是如何工作的,首先你需要理解一些使得Javascript适用于服务器端开发的关键特性.Javascript是一门简单而又灵活的语言,这种灵活性让它能够经受住时 ...

  7. [NodeJs系列][译]理解NodeJs中的Event Loop、Timers以及process.nextTick()

    译者注: 为什么要翻译?其实在翻译这篇文章前,笔者有Google了一下中文翻译,看的不是很明白,所以才有自己翻译的打算,当然能力有限,文中或有错漏,欢迎指正. 文末会有几个小问题,大家不妨一起思考一下 ...

  8. 深入理解Node.js中的垃圾回收和内存泄漏的捕获

    深入理解Node.js中的垃圾回收和内存泄漏的捕获 文章来自:http://wwsun.github.io/posts/understanding-nodejs-gc.html Jan 5, 2016 ...

  9. 如何理解Node.js和JavaScript的关系

    一.Javascript的引擎 浏览器一般有两个引擎,一个是Html引擎,一个是脚本引擎. JavaScript是一种脚本语言,最初用于浏览器的动态显示,方便操作页面数据和内容.但实际上,它也可以在浏 ...

随机推荐

  1. Java容器:Stack,Queue,PriorityQueue和BlockingQueue

    Stack Queue PriorityQueue BlockingQueue ArrayBlockingQueue LinkedBlockingQueue PriorityBlockingQueue ...

  2. Python2.7和3.5双版本共存和pip的使用

    1. Python2.7和3.5并存 1.1 安装 安装自不必多说,先装2.7,再装3.5. 说下安装的目录:Py3.5和Py2.7默认的安装目录是不一样的,按默认的来就好,不用管. Python2. ...

  3. python奇技淫巧——max/min函数的用法

    本文以max()为例,对min/max内建函数进行说明 源码 def max(*args, key=None): # known special case of max ""&qu ...

  4. Unite Beijing 2018 参会简要分享

    一. Training Day 主讲人:鲍建运 操作:马瑞 课程包括较为完整的功能,如灯光设置,角色动画控制,Cinemachine,Timeline,AI寻路,以及最新的Post Processin ...

  5. Mac下设置JAVA_HOME和MAVEN_HOME

    1.找到java安装路径 /usr/libexec/java_home 2.设置JAVA_HOME $ vim ~/.bash_profile ~/.bash_profile:每个用户都可使用该文件输 ...

  6. 计算机网络相关:应用层协议(一):DNS

    DNS 1.概念  DNS是:  1)  一个有分层的DNS服务器实现的分布式数据库  2)一个使得主机能够查询分布式数据库的应用协议.  它运行在UDP之上,默认使用53号端口.  主要功能 是将主 ...

  7. js中给easyUI年份,月份选择下拉框赋值

    sp中定义 js中初始化 //年度下拉框初始化 $("#yearChoose").combobox({    valueField:'year',     textField:'y ...

  8. linux 用户空间与内核空间——高端内存详解

    摘要:Linux 操作系统和驱动程序运行在内核空间,应用程序运行在用户空间,两者不能简单地使用指针传递数据,因为Linux使用的虚拟内存机制,用户空间的数据可能被换出,当内核空间使用用户空间指针时,对 ...

  9. 定时器Timer的使用

    概述 Timer类的主要作用是设置计划任务,但封装任务的类却是TimerTask类.执行计划任务的代码要放入TimerTask的子类中,因为TimerTask是一个抽象类. 方法schedule(ta ...

  10. 使用jmeter 进行接口的性能测试

    1.启动jmeter:在bin下以管理员身份运行jmeter.bat,启动jmeter 2. 创建测试计划: 默认启动jmeter时会加载一个测试技术模板,保存测试计划:修改名称为Apitest,点击 ...