本文结论已经废弃,从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 阶段,接下来会有几种情况:

  1. setImmediate 的 queue 不为空,则进入 check 阶段,然后是 close callbacks 阶段……
  2. setImmediate 的 queue 为空,但是 timers 的 queue 不为空,则直接进入 timers 阶段,然后又来到 poll 阶段……
  3. 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调用

参考资料1
参考资料2

 
转载自 https://www.jianshu.com/p/deedcbf68880

浏览器与NodeJS环境 eventloop异同详解(转)的更多相关文章

  1. VirtualBox开发环境的搭建详解(转)

    VirtualBox开发环境的搭建详解   有关VirtualBox的介绍请参考:VirtualBox_百度百科 由于VirtualBox官网提供的搭建方法不够详细,而且本人在它指导下,从下载所需的开 ...

  2. Linux环境fork()函数详解

    Linux环境fork()函数详解 引言 先来看一段代码吧, 1 #include <sys/types.h> 2 #include <unistd.h> 3 #include ...

  3. fabric网络环境启动过程详解

    这篇文章对fabric的网络环境启动过程进行讲解,也就是我们上节讲到的启动测试fabric网络环境时运行network_setup.sh这个文件的执行流程 fabric网络环境启动过程详解 上一节我们 ...

  4. JAVA环境变量配置详解(Windows)

    JAVA环境变量配置详解(Windows)   JAVA环境变量JAVA_HOME.CLASSPATH.PATH设置详解  Windows下JAVA用到的环境变量主要有3个,JAVA_HOME.CLA ...

  5. Scala IDEA for Eclipse里用maven来创建scala和java项目代码环境(图文详解)

    这篇博客 是在Scala IDEA for Eclipse里手动创建scala代码编写环境. Scala IDE for Eclipse的下载.安装和WordCount的初步使用(本地模式和集群模式) ...

  6. 用maven来创建scala和java项目代码环境(图文详解)(Intellij IDEA(Ultimate版本)、Intellij IDEA(Community版本)和Scala IDEA for Eclipse皆适用)(博主推荐)

    不多说,直接上干货! 为什么要写这篇博客? 首先,对于spark项目,强烈建议搭建,用Intellij IDEA(Ultimate版本),如果你还有另所爱好尝试Scala IDEA for Eclip ...

  7. 解析浏览器和nodejs环境下console.log()的区别

    写在前面的 在开发调试过程中,我们经常需要调用console.log 方法来打印出当前变量的值,然而,console.log在浏览器环境下 有时会出现一些异常的现象 开撸代码 在浏览器和nodejs环 ...

  8. 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. ...

  9. Sublime Text3 for Java 编译运行环境配置 入门详解 - 精简归纳

    Sublime Text3 for Java 编译运行环境配置 入门详解 - 精简归纳 JERRY_Z. ~ 2020 / 9 / 24 转载请注明出处!️ 目录 Sublime Text3 for ...

随机推荐

  1. Linux学习笔记(十三)磁盘管理(一):磁盘分区

    一.查看磁盘分区使用状况 df [选项] 其中,-l是默认选项 -a 会显示出包括0字节分区在内的所有文件系统分区 -t 后须跟一个参数,这个参数为文件系统的名字,用来在系统中查找属于该文件系统的分区 ...

  2. Java锁--公平锁

    转载请注明出处:http://www.cnblogs.com/skywang12345/p/3496147.html 基本概念 本章,我们会讲解“线程获取公平锁”的原理:在讲解之前,需要了解几个基本概 ...

  3. Java并发包--LinkedBlockingDeque

    转载请注明出处:http://www.cnblogs.com/skywang12345/p/3503480.html LinkedBlockingDeque介绍 LinkedBlockingDeque ...

  4. 关于c语言中结构体的初始化

    1.先定义结构体类型后再定义结构体变量: 格式为:struct 结构体名 变量名列表: struct book s1,s2,*ss://注意这种之前要先定义结构体类型后再定义变量: 2.在定义结构体类 ...

  5. Educational Codeforces Round 74 (Rated for Div. 2) A. Prime Subtraction

    链接: https://codeforces.com/contest/1238/problem/A 题意: You are given two integers x and y (it is guar ...

  6. HR#4 题解

    既然考这么差就来写题啦OTZ T1 猜结论?猜nm! 一直到考试结束都没猜出来=.=我就好奇别人如何猜出来的 我们来说DP(from ZBK) 设\(dp[i][j]\)表示胜or负 那我们来看一下代 ...

  7. 对象(Object)相关

    详情参考 1.对象的表示方法 js原生提供Object构造函数.js中所有的对象都是Object的实例. 定义一个对象最简单的就是var obj = {}; ES6属性和方法允许简写.对象的super ...

  8. scapy2 爬取全站,以及使用post请求

    前情提要: 一:scrapy 爬取妹子网 全站  知识点: scrapy回调函数的使用 二: scrapy的各个组件之间的关系解析 Scrapy 框架 Scrapy是用纯Python实现一个为了爬取网 ...

  9. 团队开发前端VUE项目代码规范

    团队开发前端VUE项目代码规范 2018年09月22日 20:18:11 我的小英短 阅读数 1658   一.规范目的: 统一编码风格,命名规范,注释要求,在团队协作中输出可读性强,易维护,风格一致 ...

  10. MySQL5.7 (审计)安装audit审计插件

    转载自:https://blog.51cto.com/13941177/2173086 注意: 安装插件的方式优缺点: 缺点:日志信息比较大,对性能影响大. 优点:对每一时刻每一用户的操作都有记录. ...