最近对Event loop比较感兴趣,所以了解了一下。但是发现整个Event loop尽管有很多篇文章,但是没有一篇可以看完就对它所有内容都了解的文章。大部分的文章都只阐述了浏览器或者Node二者之一,没有对比的去看的话,认识总是浅一点。所以才有了这篇整理了百家之长的文章。

1. 定义

Event loop:为了协调事件(event),用户交互(user interaction),脚本(script),渲染(rendering),网络(networking)等,用户代理(user agent)必须使用事件循环(event loops)。

那什么是事件?

事件:事件就是由于某种外在或内在的信息状态发生的变化,从而导致出现了对应的反应。比如说用户点击了一个按钮,就是一个事件;HTML页面完成加载,也是一个事件。一个事件中会包含多个任务。

我们在之前的文章中提到过,JavaScript引擎又称为JavaScript解释器,是JavaScript解释为机器码的工具,分别运行在浏览器和Node中。而根据上下文的不同,Event loop也有不同的实现:其中Node使用了libuv库来实现Event loop; 而在浏览器中,html规范定义了Event loop,具体的实现则交给不同的厂商去完成。

所以,浏览器的Event loop和Node的Event loop是两个概念,下面分别来看一下。

2. 意义

在实际工作中,了解Event loop的意义能帮助你分析一些异步次序的问题(当然,随着es7 async和await的流行,这样的机会越来越少了)。除此以外,它还对你了解浏览器和Node的内部机制有积极的作用;对于参加面试,被问到一堆异步操作的执行顺序时,也不至于两眼抓瞎。

3. 浏览器上的实现

在JavaScript中,任务被分为Task(又称为MacroTask,宏任务)和MicroTask(微任务)两种。它们分别包含以下内容:

MacroTask: script(整体代码), setTimeout, setInterval, setImmediate(node独有), I/O, UI rendering
MicroTask: process.nextTick(node独有), Promises, Object.observe(废弃), MutationObserver

需要注意的一点是:在同一个上下文中,总的执行顺序为同步代码—>microTask—>macroTask[6]。这一块我们在下文中会讲。

浏览器中,一个事件循环里有很多个来自不同任务源的任务队列(task queues),每一个任务队列里的任务是严格按照先进先出的顺序执行的。但是,因为浏览器自己调度的关系,不同任务队列的任务的执行顺序是不确定的。

具体来说,浏览器会不断从task队列中按顺序取task执行,每执行完一个task都会检查microtask队列是否为空(执行完一个task的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有microtask。然后再进入下一个循环去task队列中取下一个task执行,以此类推。

注意:图中橙色的MacroTask任务队列也应该是在不断被切换着的。

详见《什么是浏览器的事件循环(Event Loop)

4. Node上的实现

nodejs的event loop分为6个阶段,它们会按照顺序反复运行,分别如下:

  1. timers:执行setTimeout() 和 setInterval()中到期的callback。
  2. I/O callbacks:上一轮循环中有少数的I/Ocallback会被延迟到这一轮的这一阶段执行
  3. idle, prepare:队列的移动,仅内部使用
  4. poll:最为重要的阶段,执行I/O callback,在适当的条件下会阻塞在这个阶段
  5. check:执行setImmediate的callback
  6. close callbacks:执行close事件的callback,例如socket.on("close",func)

不同于浏览器的是,在每个阶段完成后,而不是MacroTask任务完成后,microTask队列就会被执行。这就导致了同样的代码在不同的上下文环境下会出现不同的结果。我们在下文中会探讨。

另外需要注意的是,如果在timers阶段执行时创建了setImmediate则会在此轮循环的check阶段执行,如果在timers阶段创建了setTimeout,由于timers已取出完毕,则会进入下轮循环,check阶段创建timers任务同理。

5. 示例

5.1 浏览器与Node执行顺序的区别

setTimeout(()=>{
console.log('timer1') Promise.resolve().then(function() {
console.log('promise1')
})
}, 0) setTimeout(()=>{
console.log('timer2') Promise.resolve().then(function() {
console.log('promise2')
})
}, 0) 浏览器输出:
time1
promise1
time2
promise2 Node输出:
time1
time2
promise1
promise2
 

在这个例子中,Node的逻辑如下:

最初timer1和timer2就在timers阶段中。开始时首先进入timers阶段,执行timer1的回调函数,打印timer1,并将promise1.then回调放入microtask队列,同样的步骤执行timer2,打印timer2;
至此,timer阶段执行结束,event loop进入下一个阶段之前,执行microtask队列的所有任务,依次打印promise1、promise2。

而浏览器则因为两个setTimeout作为两个MacroTask, 所以先输出timer1, promise1,再输出timer2,promise2。

更加详细的信息可以查阅《深入理解js事件循环机制(Node.js篇)

为了证明我们的理论,把代码改成下面的样子:


setImmediate(() => {
console.log('timer1') Promise.resolve().then(function () {
console.log('promise1')
})
}) setTimeout(() => {
console.log('timer2') Promise.resolve().then(function () {
console.log('promise2')
})
}, 0) Node输出:
timer1 timer2
promise1 或者 promise2
timer2 timer1
promise2 promise1

 

按理说setTimeout(fn,0)应该比setImmediate(fn)快,应该只有第二种结果,为什么会出现两种结果呢?
这是因为Node 做不到0毫秒,最少也需要1毫秒。实际执行的时候,进入事件循环以后,有可能到了1毫秒,也可能还没到1毫秒,取决于系统当时的状况。如果没到1毫秒,那么 timers 阶段就会跳过,进入 check 阶段,先执行setImmediate的回调函数。

另外,如果已经过了Timer阶段,那么setImmediate会比setTimeout更快,例如:


const fs = require('fs');

fs.readFile('test.js', () => {
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
});

上面代码会先进入 I/O callbacks 阶段,然后是 check 阶段,最后才是 timers 阶段。因此,setImmediate才会早于setTimeout执行。

具体可以看《Node 定时器详解》。

5.2 不同异步任务执行的快慢


setTimeout(() => console.log(1));
setImmediate(() => console.log(2)); Promise.resolve().then(() => console.log(3));
process.nextTick(() => console.log(4)); 输出结果:4 3 1 2或者4 3 2 1

因为我们上文说过microTask会优于macroTask运行,所以先输出下面两个,而在Node中process.nextTick比Promise更加优先[3],所以4在3前。而根据我们之前所说的Node没有绝对意义上的0ms,所以1,2的顺序不固定。

5.3 MicroTask队列与MacroTask队列


setTimeout(function () {
console.log(1);
},0);
console.log(2);
process.nextTick(() => {
console.log(3);
});
new Promise(function (resolve, rejected) {
console.log(4);
resolve()
}).then(res=>{
console.log(5);
})
setImmediate(function () {
console.log(6)
})
console.log('end'); Node输出:
2 4 end 3 5 1 6


这个例子来源于《JavaScript中的执行机制》。Promise的代码是同步代码,then和catch才是异步的,所以4要同步输出,然后Promise的then位于microTask中,优于其他位于macroTask队列中的任务,所以5会优于1,6输出,而Timer优于Check阶段,所以1,6。

6. 总结

综上,关于最关键的顺序,我们要依据以下几条规则:

  1. 同一个上下文下,MicroTask会比MacroTask先运行
  2. 然后浏览器按照一个MacroTask任务,所有MicroTask的顺序运行,Node按照六个阶段的顺序运行,并在每个阶段后面都会运行MicroTask队列
  3. 同个MicroTask队列下process.tick()会优于Promise

Event loop还是比较深奥的,深入进去会有很多有意思的东西,有任何问题还望不吝指出。

参考文档:

  1. 什么是浏览器的事件循环(Event Loop)
  2. 不要混淆nodejs和浏览器中的event loop
  3. Node 定时器详解
  4. 浏览器和Node不同的事件循环(Event Loop)
  5. 深入理解js事件循环机制(Node.js篇)
  6. JavaScript中的执行机制

[转]Event loop——浏览器和Node区别的更多相关文章

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

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

  2. 浅析Node.js的Event Loop

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

  3. Node.js 事件循环(Event Loop)介绍

    Node.js 事件循环(Event Loop)介绍 JavaScript是一种单线程运行但又绝不会阻塞的语言,其实现非阻塞的关键是“事件循环”和“回调机制”.Node.js在JavaScript的基 ...

  4. The Node.js Event Loop, Timers, and process.nextTick() Node.js事件循环,定时器和process.nextTick()

    个人翻译 原文:https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ The Node.js Event Loop, Ti ...

  5. The Node.js Event Loop, Timers, and process.nextTick()

    The Node.js Event Loop, Timers, and process.nextTick() | Node.js https://nodejs.org/uk/docs/guides/e ...

  6. Event Loop详解

    1.进程,单线程与多线 进程: 运行的程序就是一个进程,比如你正在运行的浏览器,它会有一个进程. 线程: 程序中独立运行的代码段. 一个进程由单个或多个线程组成,线程是负责执行代码的. 2.单线程与多 ...

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

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

  8. Event Loop 是什么?

    Event Loop 是什么? 本文写于 2020 年 12 月 6 日 广义上来说 Event Loop 并不是 JavaScript 独有的概念,他是一个计算机的通用概念. 狭义上来说,只有 No ...

  9. nodejs(五)同步异步--BLOCKING THE EVENT LOOP

    1.BLOCKING THE EVENT LOOP Node and JavaScript runtimes in general are single-threaded event loops. O ...

随机推荐

  1. 用 Docker 搭建Sha--dow--sock--s 笔记

    首先得找一台海外服务器,该服务器一定要在海外. 一.在https://my.vultr.com/购买一台海外服务器,亲测选美国Miami速度最稳: 二.系统我选了CentOS 7 x64,在CentO ...

  2. Leetcode92. Reverse Linked List II反转链表

    反转从位置 m 到 n 的链表.请使用一趟扫描完成反转. 说明: 1 ≤ m ≤ n ≤ 链表长度. 示例: 输入: 1->2->3->4->5->NULL, m = 2 ...

  3. LinkedHashMap笔记

    一.最基本元素存储单元 /** * HashMap.Node subclass for normal LinkedHashMap entries. */ static class Entry<K ...

  4. sql.xml where ids in的写法

    <if test="iSurfaceTypeArray != null"> AND b.i_SurfaceType in <!-- 根据外观检查查询 --> ...

  5. Android 之 BroadcaseReceiver

    1.在AndroidManifest.xml中注册 <receiver android:name=".MyReceiver"> <intent-filter &g ...

  6. vue实现分环境打包步骤(给不同的环境配置相对应的打包命令)

    在新建好的项目中,一般执行npm run build就是打包了,但此时只能打包到一个环境,不同环境需要配置不同的地址,可以手动更改接口的地址,也可以自行配置命令而不需要每次打包进行地址切换,步骤如下: ...

  7. Android之LinearLayout线性布局

    1.相关术语解释 orientation 子控件的排列方式,horizontal(水平)和vertical(垂直默认)两种方式! gravity 子控件的对齐方式,多个组合 layout_gravit ...

  8. 【JEECG-Boot 技术文档】新手入门教程

    Jeecg-Boot入门教程必看(新手学习) 1.开发环境搭建 http://jeecg-boot.mydoc.io/?t=345670 开发工具 :https://pan.baidu.com/sha ...

  9. SpringBoot学习笔记(二):SpringBoot访问静态文件、捕获全局异常、集成Thymeleaf、集成JSP

    SpringBoot访问静态文件 什么是静态文件? 不需要通过web容器去得到的文件,直接通过路径就能得到的文件,比如项目的css,js,img等文件. 所有的资源文件都应该在src/main/res ...

  10. Linux文件句柄数配置

    1.单程序句柄数限制 查看配置的句柄数:ulimit -n cat /etc/security/limits.conf 参考配置: * soft nofile 655360* hard nofile ...