浏览器与NodeJS环境 eventloop异同详解(转)
本文结论已经废弃,从node11版本开始nodejs的表现和浏览器是相同的,
都是一个宏任务,所有微任务,一个宏任务,所有微任务。
结论:浏览器中是一个宏任务,所有微任务,一个宏任务,所有微任务...
NodeJS中,一种宏任务队列所有任务,所有微任务,一种宏任务队列所有任务,所有微任务...
┌───────────────────────┐
┌─>│ timers │<————— 执行 setTimeout()、setInterval() 的回调
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
│ ┌──────────┴────────────┐
│ │ pending callbacks │<————— 执行由上一个 Tick 延迟下来的 I/O 回调(待完善,可忽略)
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
│ ┌──────────┴────────────┐
│ │ idle, prepare │<————— 内部调用(可忽略)
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
| | ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │ - (执行几乎所有的回调,除了 close callbacks 以及 timers 调度的回调和 setImmediate() 调度的回调,在恰当的时机将会阻塞在此阶段)
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ | | |
| | └───────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
| ┌──────────┴────────────┐
│ │ check │<————— setImmediate() 的回调将会在这个阶段执行
│ └──────────┬────────────┘
| |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
│ ┌──────────┴────────────┐
└──┤ close callbacks │<————— socket.on('close', ...)
└───────────────────────┘
对比浏览器
想理解整个 loop 的过程,我们可以参照浏览器的 event loop,因为浏览器的比较简单,如下:
┌───────────────────────┐
┌─>│ timers │<————— 执行一个 MacroTask Queue 的回调
│ └──────────┬────────────┘
| |<-- 执行所有 MicroTask Queue 的回调
| ────────────┘
是不是相比之下非常简洁,就这么两种 task queue,简单的一笔!
用一句话总结浏览器的 event loop 就是:
先执行一个 MacroTask,然后执行所有的 MicroTask;
再执行一个 MacroTask,然后执行所有的 MicroTask;
……
如此反复,无穷无尽……
注:可以把 script 标签中的初始同步代码视为一个初始的 MacroTask
解析
其实nodejs与浏览器的区别,就是nodejs的 MacroTask 分好几种,而这好几种又有不同的 task queue,而不同的 task queue 又有顺序区别,而 MicroTask 是穿插在每一种【注意不是每一个!】MacroTask 之间的。
其实图中已经画的很明白:
setTimeout/setInterval 属于 timers 类型;
setImmediate 属于 check 类型;
socket 的 close 事件属于 close callbacks 类型;
其他 MacroTask 都属于 poll 类型。
process.nextTick 本质上属于 MicroTask,但是它先于所有其他 MicroTask 执行;
所有 MicroTask 的执行时机,是不同类型 MacroTask 切换的时候。
idle/prepare 仅供内部调用,我们可以忽略。
pending callbacks 不太常见,我们也可以忽略。
所以我们可以按照浏览器的经验得出一个结论:
先执行所有类型为 timers 的 MacroTask,然后执行所有的 MicroTask(注意 NextTick 要优先哦);
进入 poll 阶段,执行几乎所有 MacroTask,然后执行所有的 MicroTask;
再执行所有类型为 check 的 MacroTask,然后执行所有的 MicroTask;
再执行所有类型为 close callbacks 的 MacroTask,然后执行所有的 MicroTask;
至此,完成一个 Tick,回到 timers 阶段;
……
如此反复,无穷无尽……
为了验证这个结论,我们甚至可以举一个例子:
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
此代码在浏览器环境会输出什么呢?
timer1
promise1
timer2
promise2
但是 nodejs 会输出:
timer1
timer2
promise1
promise2
如果你已经理解了上面的现象,那我们已经算基本了解 nodejs 的 event loop 了,但是其中还有一点细节
细节一:setTimeout 与 setImmediate 的顺序
本来这不应该成为一个问题,因为在文首显而易见,timers 是在 check 之前的。
但事实上,Node 并不能保证 timers 在预设时间到了就会立即执行,因为 Node 对 timers 的过期检查不一定靠谱,它会受机器上其它运行程序影响,或者那个时间点主线程不空闲。比如下面的代码,setTimeout() 和 setImmediate() 都写在 Main 进程中,但它们的执行顺序是不确定的:
setTimeout(() => {
console.log('timeout')
}, 0)
setImmediate(() => {
console.log('immediate')
})
虽然 setTimeout 延时为 0,但是一般情况 Node 把 0 会设置为 1ms,所以,当 Node 准备 event loop 的时间大于 1ms 时,进入 timers 阶段时,setTimeout 已经到期,则会先执行 setTimeout;反之,若进入 timers 阶段用时小于 1ms,setTimeout 尚未到期,则会错过 timers 阶段,先进入 check 阶段,而先执行 setImmediate
但有一种情况,它们两者的顺序是固定的:
const fs = require('fs')
fs.readFile('test.txt', () => {
console.log('readFile')
setTimeout(() => {
console.log('timeout')
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
和之前情况的区别在于,此时 setTimeout 和 setImmediate 是写在 I/O callbacks 中的,这意味着,我们处于 poll 阶段,然后是 check 阶段,所以这时无论 setTimeout 到期多么迅速,都会先执行 setImmediate。本质上是因为,我们从 poll 阶段开始执行,而非一个 Tick 的初始阶段
细节二:poll 阶段
poll 阶段主要有两个功能:
- 获取新的 I/O 事件,并执行这些 I/O 的回调,之后适当的条件下 node 将阻塞在这里
- 当有 immediate 或已超时的 timers,执行它们的回调
poll 阶段用于获取并执行几乎所有 I/O 事件回调,是使得 node event loop 得以无限循环下去的重要阶段。所以它的首要任务就是同步执行所有 poll queue 中的所有 callbacks 直到 queue 被清空或者已执行的 callbacks 达到一定上限,然后结束 poll 阶段,接下来会有几种情况:
- setImmediate 的 queue 不为空,则进入 check 阶段,然后是 close callbacks 阶段……
- setImmediate 的 queue 为空,但是 timers 的 queue 不为空,则直接进入 timers 阶段,然后又来到 poll 阶段……
- setImmediate 的 queue 为空,timers 的 queue 也为空,此时会阻塞在这里,因为无事可做,也确实没有循环下去的必要
细节三:关于 pending callbacks 阶段
在很多文章中,将 pending callbacks 阶段都写作 I/O callbacks 阶段,并说在此阶段,执行了除 close callbacks、 timers、setImmediate以外的几乎所有的回调,也就是把 poll 阶段的工作与此阶段的工作混淆了。
在我阅读时,就曾产生过疑问,假如大部分回调是在 I/O callbacks 阶段执行的,那么 poll 阶段就没有理由阻塞,因为你并不能保证“无事可做”,你得去 I/O callbacks 阶段检查一下才知道嘛!
所以最终结合其他几篇文章以及对源码的分析,应该可以确定,I/O callbacks 更准确的叫做 pending callbacks,它所执行的回调是比较特殊的、且不需要关心的,而真正重要的、大部分回调所执行的阶段是在 poll 阶段。
关于 pending callbacks 有如下说法,可以作为参考
查阅了libuv 的文档后发现,在 libuv 的 event loop 中,
I/O callbacks阶段会执行Pending callbacks。绝大多数情况下,在poll阶段,所有的 I/O 回调都已经被执行。但是,在某些情况下,有一些回调会被延迟到下一次循环执行。也就是说,在I/O callbacks阶段执行的回调函数,是上一次事件循环中被延迟执行的回调函数。
严格来说,i/o callbacks并不是处理文件i/o的callback 而是处理一些系统调用错误,比如网络 stream, pipe, tcp, udp通信的错误callback。参考 因为,pending_queue的入列(queue_insert_tail)是通过一个叫 uv__io_feed 的api来调用的 而 uv__io_feed API是在tcp/udp/stream/pipe等相关API调用
浏览器与NodeJS环境 eventloop异同详解(转)的更多相关文章
- VirtualBox开发环境的搭建详解(转)
VirtualBox开发环境的搭建详解 有关VirtualBox的介绍请参考:VirtualBox_百度百科 由于VirtualBox官网提供的搭建方法不够详细,而且本人在它指导下,从下载所需的开 ...
- Linux环境fork()函数详解
Linux环境fork()函数详解 引言 先来看一段代码吧, 1 #include <sys/types.h> 2 #include <unistd.h> 3 #include ...
- fabric网络环境启动过程详解
这篇文章对fabric的网络环境启动过程进行讲解,也就是我们上节讲到的启动测试fabric网络环境时运行network_setup.sh这个文件的执行流程 fabric网络环境启动过程详解 上一节我们 ...
- JAVA环境变量配置详解(Windows)
JAVA环境变量配置详解(Windows) JAVA环境变量JAVA_HOME.CLASSPATH.PATH设置详解 Windows下JAVA用到的环境变量主要有3个,JAVA_HOME.CLA ...
- Scala IDEA for Eclipse里用maven来创建scala和java项目代码环境(图文详解)
这篇博客 是在Scala IDEA for Eclipse里手动创建scala代码编写环境. Scala IDE for Eclipse的下载.安装和WordCount的初步使用(本地模式和集群模式) ...
- 用maven来创建scala和java项目代码环境(图文详解)(Intellij IDEA(Ultimate版本)、Intellij IDEA(Community版本)和Scala IDEA for Eclipse皆适用)(博主推荐)
不多说,直接上干货! 为什么要写这篇博客? 首先,对于spark项目,强烈建议搭建,用Intellij IDEA(Ultimate版本),如果你还有另所爱好尝试Scala IDEA for Eclip ...
- 解析浏览器和nodejs环境下console.log()的区别
写在前面的 在开发调试过程中,我们经常需要调用console.log 方法来打印出当前变量的值,然而,console.log在浏览器环境下 有时会出现一些异常的现象 开撸代码 在浏览器和nodejs环 ...
- idea spring+springmvc+mybatis环境配置整合详解
idea spring+springmvc+mybatis环境配置整合详解 1.配置整合前所需准备的环境: 1.1:jdk1.8 1.2:idea2017.1.5 1.3:Maven 3.5.2 2. ...
- Sublime Text3 for Java 编译运行环境配置 入门详解 - 精简归纳
Sublime Text3 for Java 编译运行环境配置 入门详解 - 精简归纳 JERRY_Z. ~ 2020 / 9 / 24 转载请注明出处!️ 目录 Sublime Text3 for ...
随机推荐
- 用js刷剑指offer(旋转数组的最小数字)
题目描述 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转. 输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素. 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个 ...
- C#中设置密码框 ,用符号代替密码
添加控件 添加控件 确认键代码 private void button1_Click(object sender, EventArgs e) { //修改密码.新密码,重复新密码,两次输入的新密码必须 ...
- Python3+Appium学习笔记05-报错及解决方法
记录一下使用期间各种报错和解决方法,毕竟搜了半天才找到解决方法.另外提示一下.优先看官方文档. 报错前面都是一样,就是说在处理的时候,服务器发生了一个未知的错误.然后才是具体报错信息 1)seleni ...
- rabbitmq可靠性
本文翻译汇总自rabbitmq的官方文档. 翻译使用谷歌翻译后简单修改,部分内容读起来仍然比较晦涩,不过意思传达到了. 可靠性指南 本页介绍了如何使用AMQP和RabbitMQ的各种功能来实现可靠 ...
- python_网络编程socket(UDP)
服务端: import socket sk = socket.socket(type=socket.SOCK_DGRAM) #创建基于UDP协议的socket对象 sk.bind(('127.0.0. ...
- linux实操_shell判断语句
常用判断条件: (1)两个数的比较 = 字符串的比较 -lt 小于 -le 小于等于 -eq 等于 -gt 大于 -ge 大于等于 -ne 不等于 (2)按照文件权限进行判断 -r 有读的权限 -w ...
- 一款超好用的第三方评论插件--Gittalk
使用GITALK的背景: 1. 最近在做一个基于Java的个人博客系统,已经基本完工了,突然发现怎么没有评论的操作,如果再从头开始从数据库开始写的话,花费的代价有点大,于是乎我就在网上寻找一款适合我的 ...
- BZOJ 3218 A + B Problem (可持久化线段树+最小割)
做法见dalao博客 geng4512的博客, 思路就是用线段树上的结点来进行区间连边.因为有一个只能往前面连的限制,所以还要可持久化.(duliu) 一直以来我都是写dinicdinicdinic做 ...
- HDU 6143 - Killer Names | 2017 Multi-University Training Contest 8
/* HDU 6143 - Killer Names [ DP ] | 2017 Multi-University Training Contest 8 题意: m个字母组成两个长为n的序列,两序列中 ...
- python--第四天练习题
#1.写函数,利用递归获取斐波那契数列中的第 10 个数,并将该值返回给调用者. def rec(a,b,dep=1): c = a + b if dep == 10: return c return ...