前言

本文内容比较长,请见谅。如有评议,还请评论区指点,谢谢大家!

>> 目录

  1. 开门见山:Node和浏览器的异步执行顺序问题
  2. 两种环境下的宏任务和微任务(macrotask && microtask)
  3. Node和浏览器的事件循环模型在实现层面的区别
  4. Node和浏览器的事件循环的任务队列(task queue)
  5. Node和浏览器的事件循环模型在表现层面的差异
  6. 理清libuv的“7队列”和Node“6队列”的关系
  7. Node和浏览器环境下setTimeout的最小延迟时间
  8. setTimeout和setImmediate的执行顺序详解
  9. Node相关组成结构中涉及的数据结构

一.开门见山:Node和浏览器的异步执行顺序问题

>> Node端的异步执行顺序

Node端的异步执行顺序如下
同步代码 > process.nextTick > Promise.then中的函数 > setTimeOut(0) 或 setImmediate
  • 「备注1」 Promise中的函数,无论是resolve前的还是后的,都属于“同步代码”的范围,并不是“异步代码”
  • 「备注2」 setTimeOut(0) 或 setImmediate的执行顺序取决于具体情况,并没有确定的先后区分

>> Node端异步逻辑顺序实验论证

setTimeout (function () {
console.log ('setTimeout');
}, 0);
setImmediate (function () {
console.log ('setImmediate');
});
new Promise (function (resolve, reject) {
resolve ();
}).then (function () {
console.log ('promise.then');
});
process.nextTick (function () {
console.log ('next nick');
});
console.log ('同步代码');

输出

 
备注1: Promise接收的函数的同步问题(实验论证)
console.log ('我是同步代码');
new Promise (function (resolve, reject) {
console.log ('resolve前');
resolve ();
console.log ('resolve后');
}).then (function () {});
console.log ('我是同步代码');
 
备注2: setTimeOut(0) 或 setImmediate的执行顺序问题
这个问题比较复杂,可参考下面这篇文章
 

>> 浏览器的异步执行顺序问题

浏览器中,涉及的异步API有:Promise, setTomeOut,setImmediate
(其中setImmediate可以忽略不计,因为它只在egde和IE11才支持,没错,Chrome和火狐都是不支持的,所以当然也不建议使用)
执行顺序
Promise.then中的函数 > setTimeOut(0) 或 setImmediate
以下代码
setTimeout (function () {
console.log ('setTimeout');
}, 0);
setImmediate (function () {
console.log ('setImmediate');
});
new Promise (function (resolve, reject) {
resolve ();
}).then (function () {
console.log ('promise');
});
 
在edge浏览器中的测试结果为
 

>> 参考资料

二.两种环境下的宏任务和微任务阵营(macrotask && microtask)

我们上面讲述了不同的程序,它们的异步执行顺序的区别,其中我们发现,有的异步API执行快,而有的异步API执行慢,实际上,它们作为异步任务,被分成了宏任务和微任务两大阵营,同时整体表现出微任务执行快于宏任务的现象

在宏任务和微任务方面,Node和浏览器也是差异很大的,这是因为它们的底层实现不一样。具体原理会在下面讲解,下面先概述下两种环境下的task的差别

>> 浏览器端的宏任务和微任务

下面简单介绍下宏任务和微任务的阵营
  • 宏任务(macrotasks):setTimeout, setInterval, I/O,setImmediate(如果存在),requestAnimationFrame(存在争议)
  • 微任务 (microtasks) : process.nextTick, Promises,MutationObserver

>> 备注解释

  • 备注1:MutationObserver是HTML5新增的用来检测DOM变化的,参考资料

  • 备注2: 部分资料认为,requestAnimationFrame也属于宏任务,理由是:requestAnimationFrame在MDN的定义为,下次页面重绘前所执行的操作,而重绘也是作为宏任务的一个步骤来存在的,且该步骤晚于微任务的执行,参考资料

>> Node端的宏任务和微任务

(⚠️该概念定义可能存在争议,部分资料对Node中也做了宏任务和微任务的划分,而部分资料则只提出了微任务的概念,而没有涉及宏任务,本文遵从前者)
  • 微任务:process.nextTick,promise.then
  • 宏任务:setTimeout, setInterval,setImmediate
当然了,直接说宏任务的执行比微任务的解释也许太粗糙了,没办法解释很多具体的问题,比如:具体不同的宏任务之间的顺序问题,所以,要做进一步的判断,我们就要理解JS事件循环中的执行阶段,和队列相关的知识
 

三.Node和浏览器的事件循环模型在实现层面的区别

浏览器的事件循环是在 HTML5 中定义的规范,而 Node 中则是由 libuv 库实现,这是它们在实现上的根本差别。也就是说,很多时候,他们的行为看起来很像,但event loop的内在实现却存在差别。

>> 浏览器的event loop

我们看下规范的定义,以下援引自HTML5规范草案
To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. Each agent has an associated event loop.
“为了协调事件,用户交互,脚本,渲染,网络等,用户代理(浏览器)必须使用本节中描述的事件循环。每个代理都有一个关联的事件循环。”
也就是说,浏览器根据这个草案的规定,实现了事件循环,目的是用来协调浏览器的事件,交互和渲染的。
 

>> Node的event loop

Node的事件循环基于libuv实现,libuv是Node.js的底层依赖,一个跨平台的异步IO库。分别通过windows平台下的IOCP和Unix 环境下的 libev实现跨平台的兼容。
实际上,虽然libuv作为Node的底层模块,一开始是为了Node而设计的,但是它被抽象了出来,并且不仅仅为Node服务,也服务于其他语言,例如,它也支持了julia等语言的实现(Julia 是一个面向科学计算的语言)
 

四.Node和浏览器的事件循环的任务队列

>> 参考资料

>> Node的任务队列

Node的任务队列总共6个:包括4个主队列(main queue)和两个中间队列(intermediate queue)

  • 四个主队列由libuv提供
  • 两个中间队列由Node.js实现
(⚠️上面这个论断我是根据相关资料推断的,如有不当请指正)

>> 6个队列具体内容

  • 主队列(main queue):包括计时器队列,IO事件队列,即时队列,关闭事件处理程序队列
  • 中间队列(intermediate queue):包括(1)Next Ticks队列和(2)其他微任务队列
(此概念 由Deepal Jayasekara,一位德国Node开发者提出,即上面文章的作者)

>> 四个主队列

Q1.计时器队列 (timer queue)
在计数器队列中,Node会在这里保存setTimeOut和setInterval添加的处理程序,所以处理到这个队列的时候,Node会在一堆计时器中检查有没有过期的计时器,如果过期了,就调用其这个计时器的回调函数。如果有多个计时器到期(设置了相同的到期时间),那么会根据设置的先后,按照顺序去执行它们。
从这里也可以看出,为什么我们总会强调setTimeOut和setInterval的时间误差。这是因为只有在该循环流程中,检查到“过期”了,才会对计时器进行处理
 
Q2.IO事件队列(IO events queue)
IO一般指的是和CPU以外的外部设备通信的工作,例如文件操作和TCP/UDP网络操作等。
Node依赖于底层模块libuv提供的异步IO的功能。在IO事件队列中,Node将处理所有待处理的I/O操作
 
Q3.即时队列 (immediate queue)
处理这个队列的时候,setImmediate设置的函数回调,会被依次调用
 
Q4.关闭事件处理程序(close handlers queue)
当处理到这个队列的时候,Node将会处理所有I / O事件处理程序
 

>> 两个中间队列

Q5.next ticks队列
保存process.nextTick调用形成的任务
 
Q6.其他微任务队列
保存Promise形成的任务
 

>> 主队列和中间队列的关系

在一轮循环中,4个主队列,每处理完一个主队列,接着就要把两个中间队列处理一次, 我的理解是:一趟循环走下来, 4个主队列都各自被处理了一次,而2个中间队列则是被处理了4次。
 
图示如下
 
这个图可能说的不是很清楚,所以我整理了一下,如下所示:
(备注⚠️:此图只适用于Node11.0.0版本以前的情况! 对于Node11以后的队列执行流程,请参考下面一节)
 

>> 浏览器的任务队列

浏览器中只分两种队列:
  • 宏任务队列(macro task)
  • 微任务队列。(micro task)
他们的处理顺序是
  1. 每次从宏任务队列中取一个宏任务执行, 完成后, 把微任务队列中的所有微任务,一次性处理完
  2. 不断重复上述过程
如下图所示
 

五.Node和浏览器的事件循环模型在表现层面的差异

Node和浏览器的区别情况是:
  • 在Node11.0.0以前的版本,Node和浏览器的异步流程存在一些细节上的差异,
  • 但在Node11.0.0以后,这一差异被抹去了,因为Node主动修改了实现以和浏览器保持一致
吐槽:听话的Node.js
修改前后区别在于
  • 在浏览器和Node11以后,每执行完一个timer类回调,例如setTimeout,setImmediate 之后,都会把微任务给执行掉(promise等)。
  • 原来Node10和以前: 当一个任务队列(例如timer queue)里面的回调都批量执行完了,才去执行微任务
我们可以看出,微任务的执行变得更迅速了,不再是跟在任务队列处理完后处理,而是在单个timer类回调(setTimeout,setImmediate)处理完后,也会被处理了。
 
让我们分析下面这段代码
setTimeout (function () {
console.log ('timeout1:宏任务');
new Promise (function (resolve, reject) {
resolve ();
}).then (() => {
console.log ('promise:微任务');
});
});
setTimeout (function () {
console.log ('timeout2:宏任务');
});

对这段代码
  • 如果是11以后的Node和浏览器:执行完第一个setTimeout后,接下来轮到Promise这类微任务执行了,所以接下来应该是输出「promise:微任务」
  • 如果是version11以前的Node,则执行完第一个setTimeout后,因为timer队列没处理完,所以接下来执行的是第二个setTimeout,输出的是「timeout2:宏任务」
运行结果
浏览器
Node10.16.3(nvm切换node版本)
Node11.0.0(nvm切换node版本)
我们不难发现其中差别,Node10.16.3的表现是和浏览器不一样的,而到了Node11,则Node和浏览器相一致了。
 

>> 参考资料

 

六.理清libuv的“七队列”和Node“四个主队列”的关系

(⚠️下面的是个人理解,如有您有更合理的观点,请在评论区给出,谢谢)
好吧,其实上面的内容已经有点复杂了! 可是这个时候,又有个神奇的概念过来插一脚 它就是,Node官方文档里面提出的“七队列”
下面介绍一下这位小伙伴

>> 我们首先要明白的是三点

  1. 这里的七队列是libuv内部的概念
  2. 之前介绍的"Node六队列"和"四个主队列"是Node内部,但在libuv外部的实现和概念
  3. 这两者之间存在对应关系,虽然不是一一对应(下面会细讲对应关系)

>> libuv七队列图解

 

>> 七队列的具体作用

  • timers:执行满足条件的 setTimeout 、setInterval 回调;

  • pending callbacks: 检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,它们由计时器和 setImmediate() 排定的之外),其余情况 node 将在此处阻塞。

  • idle:仅仅供给Node系统内部使用
  • prepare:仅仅供给Node系统内部使用
  • poll:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,它们由计时器和 setImmediate() 排定的之外),其余情况 node 将在此处阻塞。
  • check:执行 setImmediate 的回调;
  • close callbacks:关闭所有的 closing handles ,一些 onclose 事件;

>> libuv七队列和Node四个主队列的对应关系

 

>> 参考资料

七.Node和浏览器环境下setTimeout的最小延迟时间

>> 浏览器端的最小延迟时间

“HTML5 规范规定最小延迟时间不能小于 4ms,即 x 如果小于 4,会被当做 4 来处理。 不过不同浏览器的实现不一样,比如,Chrome 可以设置 1ms,IE11/Edge 是 4ms。”

>> Node端的最小延迟时间

一句话足以:Node端没有最小延迟时间

>> 我觉得里面有一句话说的特别好

Node没有最小延迟,这实际上是浏览器和节点之间的兼容性问题。计时器(setTimeout和setImmediate)在JavaScript中是完全未指定的(这是DOM规范,在Node中没有用,何况浏览器也没有遵循),而node实现它们的原因仅仅是因为它们在JavaScript的历史上非常地基础
It doesn't have a minimum delay and this is actually a compatibility issue between browsers and node. Timers are completely unspecified in JavaScript (it's a DOM specification which has no use in Node and isn't even followed by browsers anyway) and node implements them simply due to how fundamental they've been in JavaScript's history
 

八.setTimeout(0 delay)和setImmediate的执行顺序详解

这个问题其实比较复杂,不能一概而论。

>> 总结来说

  • 在主线程中直接调用setTimeOut(0,function) 和setImmediate不能确定其执行的先后顺序
  • 但是如果在同一个IO循环中,例如在一个异步回调中调用这两个方法,setImmediate会首先被调用

>> 具体解释

第一.在主线程中运行以下脚本,我们不能确定timeout和immediate输出的先后顺序,结果受到进程性能的影响 (例子源于Node官方文档,链接在下面给出)

// timeout_vs_immediate.js
setTimeout(() => {
console.log('timeout');
}, 0); setImmediate(() => {
console.log('immediate');
});
结果
输出结果无法确定
 
第二.如果在一个IO循环中运行setTimeOut(0,function) 和setImmediate,那么setImmediate 总是被优先调用
// timeout_vs_immediate.js
const fs = require('fs'); fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
 
输出结果
immediate timeout

九.Node相关组成结构中涉及的数据结构

>> 介绍

  • setTimeout与setInterval: 调用这两个函数创建的定时器会被插入到定时器观察者内部的一个红黑树中,每次tick执行时候都会从红黑树中迭代取出定时器对象。

  • process.nextTick: 将回调函数放入到队列中,在下一轮Tick时取出执行,可以达到setTimeout(fn,0)的效果,由于不需要动用红黑树,效率更高时间复杂度为O(1)。相比较之下。(红黑树时间复杂度O(lg(n)) )

  • setImmediate:的回调函数保存在链表中,每次Tick只执行链表中的一个回调函数。

>> 本节参考资料

  • 《深入浅出Node.js》作者:朴灵,阿里巴巴数据平台资深开发者,被尊为Node.js的布道者

破阵九解:Node和浏览器之事件循环/任务队列/异步顺序/数据结构的更多相关文章

  1. 【nodejs原理&源码赏析(7)】【译】Node.js中的事件循环,定时器和process.nextTick

    [摘要] 官网博文翻译,nodejs中的定时器 示例代码托管在:http://www.github.com/dashnowords/blogs 原文地址:https://nodejs.org/en/d ...

  2. 【nodejs原理&源码赏析(7)】【译】Node.js中的事件循环,定时器和process.nextTick

    目录 Event Loop 是什么? Event Loop 基本解释 事件循环阶段概览 事件循环细节 timers pending callbacks poll阶段 check close callb ...

  3. JavaScript是如何工作的:事件循环和异步编程的崛起 + 5种使用 async/await 更好地编码方式!

    摘要: 深度理解JS事件循环!!! 原文:JavaScript是如何工作的:事件循环和异步编程的崛起+ 5种使用 async/await 更好地编码方式! 作者:前端小智 Fundebug经授权转载, ...

  4. c#封装DBHelper类 c# 图片加水印 (摘)C#生成随机数的三种方法 使用LINQ、Lambda 表达式 、委托快速比较两个集合,找出需要新增、修改、删除的对象 c# 制作正方形图片 JavaScript 事件循环及异步原理(完全指北)

    c#封装DBHelper类   public enum EffentNextType { /// <summary> /// 对其他语句无任何影响 /// </summary> ...

  5. 初学Node(四)事件循环

    Node中的事件循环 事件循环是Node的核心,正是因为有了事件循环JS才能够在服务端占有一席之地.JS是一种单线程语言,但是它的执行环境是多线程的在加上JS的事件驱动这一特点,使使JS在执行的过程中 ...

  6. How Javascript works (Javascript工作原理) (四) 事件循环及异步编程的出现和 5 种更好的 async/await 编程方式

    个人总结: 1.讲解了JS引擎,webAPI与event loop合作的机制. 2.setTimeout是把事件推送给Web API去处理,当时间到了之后才把setTimeout中的事件推入调用栈. ...

  7. JavaScript 是如何工作的: 事件循环和异步编程的崛起 + 5个如何更好的使用 async/await 编码的技巧 - 学习笔记

    那么,谁会告诉 JS 引擎去执行你的程序?事实上,JS 引擎不是单独运行的 —— 它运行在一个宿主环境中,对于大多数开发者来说就是典型的浏览器和 Node.js.实际上,如今,JavaScript 被 ...

  8. JavaScript 事件循环及异步原理(完全指北)

    引言 最近面试被问到,JS 既然是单线程的,为什么可以执行异步操作? 当时脑子蒙了,思维一直被困在 单线程 这个问题上,一直在思考单线程为什么可以额外运行任务,其实在我很早以前写的博客里面有写相关的内 ...

  9. JavaScript是如何工作的:事件循环和异步编程的崛起+ 5种使用 async/await 更好地编码方式!

    为什么单线程是一个限制? 在发布的第一篇文章中,思考了这样一个问题:当调用堆栈中有函数调用需要花费大量时间来处理时会发生什么? 例如,假设在浏览器中运行一个复杂的图像转换算法. 当调用堆栈有函数要执行 ...

随机推荐

  1. MySQL中几个重要的文件

    一.数据库层面 错误日志文件(error log) 二进制日志文件(binary log) 慢查询日志(slow log) 全量日志(general log):general log 会记录MySQL ...

  2. 每个Java开发人员都应该知道的10个基本工具

    大家好,我们已经在2019年的第9个月,我相信你们所有人已经在2019年学到了什么,以及如何实现这些目标.我一直在写一系列文章,为你提供一些关于你可以学习和改进的想法,以便在2019年成为一个更好的. ...

  3. Python中使用pip安装库时提示:远程主机强迫关闭了一个现有的连接

    场景 在cmd中使用pip install moviepy时,需要安装一些依赖库,很长时间后提示: 远程主机中断了一个现有的连接. 原因是默认镜像源下载过慢,将其修改为国内或者设置安装时的源. 这里以 ...

  4. 2010年NOIP普及组复赛题解

    题目及涉及的算法: 数字统计:入门题: 接水问题:基础模拟题: 导弹拦截:动态规划.贪心: 三国游戏:贪心.博弈论. 数字统计 题目链接:洛谷 P1179 这道题目是一道基础题. 我们只需要开一个变量 ...

  5. OAuth2.0摘要

    一.简介 不使用oauth2.0协议,资源所有者直接给需要使用资源的第三方应用共享凭据时,有这些问题: 需要直接共享给第三方应用凭据 需要服务器支持密码身份验证 凭据的访问权限过大,失去对访问时间和范 ...

  6. JavaScript之深入对象(一)

    在之前的<JavaScript对象基础>中,我们大概了解了对象的创建和使用,知道对象可以使用构造函数和字面量方式创建.那么今天,我们就一起来深入了解一下JavaScript中的构造函数以及 ...

  7. java数据结构——二叉树(BinaryTree)

    前面我们已经学习了一些线性结构的数据结构和算法,接下来我们开始学习非线性结构的内容. 二叉树 前面显示增.删.查.遍历方法,完整代码在最后面. /** * 为什么我们要学习树结构. * 1.有序数组插 ...

  8. Spring boot 梳理 - 模版引擎 -freemarker

    开发环境中关闭缓存 spring: thymeleaf: cache: false freemarker: cache: false Spring boot 集成 freemarker <dep ...

  9. Merge into 语句实例

    /*Merge into 详细介绍MERGE语句是Oracle9i新增的语法,用来合并UPDATE和INSERT语句.通过MERGE语句,根据一张表或子查询的连接条件对另外一张表进行查询,连接条件匹配 ...

  10. WordPress新用户注册时提示“您的密码重设链接无效”

    在使用Wordpress密码找回功能及新用户注册邮件中的重置密码链接时,Wordpress提示“您的密码重设链接无效,请在下方请求新链接.”.“该key似乎无效”.“invalid key”. 这个其 ...