从几道题目带你深入理解Event Loop_宏队列_微队列
深入探究JavaScript的Event Loop
Javascript是一门单线程语言
但是在运行时难免会遇到需要较长执行时间的任务如: 向后端服务器发送请求。 其他的任务不可能都等它执行完才执行的(同步)否则效率太低了, 于是异步的概念就此产生: 当遇到需要较长时间的任务时将其放入"某个地方"后继续执行其他同步任务, 等所有同步任务执行完毕后再poll(轮询)刚刚这些需要较长时间的任务并得到其结果
而处理异步任务的这一套流程就叫Event Loop即事件循环,是浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制, 于是更完善的说法是: Javascript是一门单线程非阻塞语言
Event Loop的结构

- 堆(heap): 用于存放JS对象的数据结构
- 调用栈(stack): 同步任务会按顺序在调用栈中等待主线程依次执行
- Web API: 是浏览器/Node 用于处理异步任务的地方
- 回调队列(callbacks queue): 经过Web API处理好的异步任务会被一次放入回调队列中, 等一定条件成立后被逐个poll(轮询)放入stack中被主线程执行
回调队列(callbacks queue)的分类
回调队列(callbacks queue)进而可以细分为
宏任务(macroTasks)- script全部代码、
- setTimeout、
- setInterval、
- setImmediate(浏览器暂时不支持,只有IE10支持,具体可见MDN)、
- I/O、UI Rendering
微任务(microTasks)- Process.nextTick(Node独有)
- MutationObserver
- Promise、
- Object.observe(废弃)
Event Loop的执行顺序
- 首先顺序执行初始化代码(run script), 同步代码放入调用栈中执行, 异步代码放入对应的队列中
- 所有同步代码执行完毕后,确认调用栈(stack)是否为空, 只有stack为为空才能开始按照队列的特性轮询执行 微任务队列中的代码
- 只有当所有微任务队列中的任务执行完后, 才能执行宏任务队列中的下一个任务
用流程图表示:

通过题目来深入
题目1:
setTimeout(() => {
console.log(1)
}, 0)
Promise.resolve().then(
() => {
console.log(2)
}
)
Promise.resolve().then(
() => {
console.log(4)
}
)
console.log(3)
执行初始化代码

初始化代码执行完毕, 调用栈为空所以可以开始轮询执行微任务队列的代码
取出第一个任务到调用栈--打印2, 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行

取出第二个任务到调用栈--打印4, 执行完后调用栈为空, 微任务队列为空, 第一个宏任务(run script)完成, 可以轮询宏任务队列的下一个任务


开始轮询执行宏任务队列中的下一个任务

于是这道题最终的结果是:
3 2 4 1
到这需要说明一个东西就是: setTimeout的回调执行是不算在run script中的, 具体原因我并未弄清, 有明白的同学欢迎解释
题目2:
setTimeout(()=>{
console.log(1)
}, 0)
new Promise((resolve, reject) => {
console.log(2)
resolve()
})
.then(
() => {
console.log(3)
}
)
.then(
() => {
console.log(4)
}
)
console.log(5)
执行初始化代码

初始化代码执行完毕, 调用栈为空所以可以开始轮询执行微任务队列的代码
- 取出第一个任务到调用栈--打印3, 执行完后调用栈为空, 此时第一个then()返回的Promise有了状态、结果, 于是将第二个then()放入微任务队列中, 检查微任务队列是否还有任务有则执行


- 调用栈、微任务队列为空, 宏任务run script执行完毕

- 取出第一个任务到调用栈--打印3, 执行完后调用栈为空, 此时第一个then()返回的Promise有了状态、结果, 于是将第二个then()放入微任务队列中, 检查微任务队列是否还有任务有则执行
开始轮询执行宏任务队列中的下一个任务

于是这道题最终的结果是:
2 5 3 4 1
题目3:
const first = () => {
return new Promise((resolve, reject) => {
console.log(3)
let p = new Promise((resolve, reject) => {
console.log(7)
setTimeout(() => {
console.log(5)
}, 0)
resolve(1)
})
resolve(2)
p.then(
arg => {
console.log(arg)
}
)
})
}
first().then(
arg => {
console.log(arg)
}
)
console.log(4)
执行初始化代码

初始化代码执行完毕, 调用栈为空所以可以开始轮询执行微任务队列的代码
- 取出第一个任务到调用栈--打印1, 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行


- 调用栈、微任务队列为空, 宏任务run script执行完毕

- 取出第一个任务到调用栈--打印1, 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行
开始轮询执行宏任务队列中的下一个任务

于是这道题最终的结果是:
3 7 4 1 2 5
题目4:
setTimeout(()=>{
console.log(0)
}, 0)
new Promise((resolve, reject) => {
console.log(1)
resolve()
})
.then(
() => {
console.log(2)
new Promise((resolve, reject) => {
console.log(3)
resolve()
})
.then(
() => console.log(4)
)
.then(
() => console.log(5)
)
}
)
.then(
() => console.log(6)
)
new Promise((resolve, reject) => {
console.log(7)
resolve()
})
.then(
() => console.log(8)
)
执行初始化代码

初始化代码执行完毕, 调用栈为空所以可以开始轮询执行微任务队列的代码
- 取出第一个任务到调用栈--执行onResolved中的所有代码, 很重要的地方是此时第一个new Promise的第二个then此时会被放入微任务队列中。 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行





- 调用栈、微任务队列为空, 宏任务run script执行完毕

- 取出第一个任务到调用栈--执行onResolved中的所有代码, 很重要的地方是此时第一个new Promise的第二个then此时会被放入微任务队列中。 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行
开始轮询执行宏任务队列中的下一个任务

于是这道题最终的结果是:
1 7 2 3 8 4 6 5 0
题目5:
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function () {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function () {
console.log('promise1')
})
.then(function () {
console.log('promise2')
})
console.log('script end')
执行初始化代码

初始化代码执行完毕, 调用栈为空所以可以开始轮询执行微任务队列的代码
- 取出第一个任务到调用栈--执行await后的所有代码, 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行



- 调用栈、微任务队列为空, 宏任务run script执行完毕

- 取出第一个任务到调用栈--执行await后的所有代码, 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行
开始轮询执行宏任务队列中的下一个任务

于是这道题最终的结果是:
script start
async2 end
Promise
script end
async1 end
promise1
promise2
setTimeout
终极题1:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<style>
.outer {
width: 200px;
height: 200px;
background-color: orange;
}
.inner {
width: 100px;
height: 100px;
background-color: salmon;
}
</style>
</head>
<body>
<div class="outer">
<div class="inner"></div>
</div>
<script>
var outer = document.querySelector('.outer')
var inner = document.querySelector('.inner')
new MutationObserver(function () {
console.log('mutate')
}).observe(outer, {
attributes: true,
})
function onClick() {
console.log('click')
setTimeout(function () {
console.log('timeout')
}, 0)
Promise.resolve().then(function () {
console.log('promise')
})
outer.setAttribute('data-random', Math.random())
}
inner.addEventListener('click', onClick)
outer.addEventListener('click', onClick)
</script>
</body>
</html>
执行初始化代码

初始化代码执行完毕, 调用栈为空所以可以开始轮询执行微任务队列的代码

- 取出第一个任务到调用栈--打印promise, 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行


- 调用栈、微任务队列为空, 因为存在冒泡, 所以以上操作再进行一次

- 取出第一个任务到调用栈--打印promise, 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行
宏任务run script执行完毕, 调用栈、微任务队列为空可以轮询执行宏任务队列中的下一个任务

开始轮询执行宏任务队列中的下一个任务

微任务队列、调用栈为空, 继续轮询执行宏任务队列中的下一个任务

于是这道题最终的结果是:
click
promise
mutate
click
promise
mutate
timeout
timeout
不同浏览器下的不同结果(如果你的结果在这其中, 也是对的)

这里令人迷惑的点是: outer的冒泡执行为什么比outer的setTimeout先
那是因为:
- 首先outer的setTimeout是一个宏任务, 它进入宏任务队列时是在了run script的后面
- inner执行到mutate后run script并没有执行完, 而是还有一个outer.click的冒泡要执行
- 只有执行完该冒泡后, run script才真正执行完(才可以执行下一个宏任务)
终极题2:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<style>
.outer {
width: 200px;
height: 200px;
background-color: orange;
}
.inner {
width: 100px;
height: 100px;
background-color: salmon;
}
</style>
</head>
<body>
<div class="outer">
<div class="inner"></div>
</div>
<script>
var outer = document.querySelector('.outer')
var inner = document.querySelector('.inner')
new MutationObserver(function () {
console.log('mutate')
}).observe(outer, {
attributes: true,
})
function onClick() {
console.log('click')
setTimeout(function () {
console.log('timeout')
}, 0)
Promise.resolve().then(function () {
console.log('promise')
})
outer.setAttribute('data-random', Math.random())
}
inner.addEventListener('click', onClick)
outer.addEventListener('click', onClick)
inner.click() // 模拟点击inner
</script>
</body>
</html>
执行初始化代码, 这里与终极题1不同的地方在于: 终极题1的click是作为回调函数(dispatch), 而这里是直接同步调用的

inner.click执行完毕, inner.click退栈, 由于调用栈并不为空, 所以不能轮询微任务队列, 而是继续执行run script(执行冒泡部分)
需要注意: 由于outer.click的MutationObserver并未执行所以不会被再次添加进微任务队列中

inner.click退栈, 宏任务run script执行完毕, run script也退栈 调用栈为空, 开始轮询微任务队列



调用栈、微任务队列为空, 开始轮询执行宏任务队列中的下一个任务

微任务队列、调用栈为空, 继续轮询执行宏任务队列中的下一个任务

于是这道题最终的结果是:
click
click
promise
mutate
promise
timeout
timeout
参考文章:
Tasks, microtasks, queues and schedules
从几道题目带你深入理解Event Loop_宏队列_微队列的更多相关文章
- hdu 动态规划(46道题目)倾情奉献~ 【只提供思路与状态转移方程】(转)
HDU 动态规划(46道题目)倾情奉献~ [只提供思路与状态转移方程] Robberies http://acm.hdu.edu.cn/showproblem.php?pid=2955 背包 ...
- Java基础 带你深刻理解自动装箱,拆箱含义
1.什么是装箱,什么是拆箱装箱:把基本数据类型转换为包装类.拆箱:把包装类转换为基本数据类型.基本数据类型所对应的包装类:int(几个字节4)- Integerbyte(1)- Byteshort(2 ...
- C语言超级经典400道题目
C语言超级经典400道题目 1.C语言程序的基本单位是____ A) 程序行 B) 语句 C) 函数 D) 字符.C.1 2.C语言程序的三种基本结构是____构A.顺序结构,选择结构,循环结 B.递 ...
- 带你深入理解STL之Set和Map
在上一篇博客带你深入理解STL之RBTree中,讲到了STL中关于红黑树的实现,理解起来比较复杂,正所谓前人种树,后人乘凉,RBTree把树都种好了,接下来就该set和map这类关联式容器来" ...
- 带你深入理解STL之Stack和Queue
上一篇博客,带你深入理解STL之Deque容器中详细介绍了deque容器的源码实现方式.结合前面介绍的两个容器vector和list,在使用的过程中,我们确实要知道在什么情况下需要选择恰当的容器来满足 ...
- 带你深入理解STL之Vector容器
C++内置了数组的类型,在使用数组的时候,必须指定数组的长度,一旦配置了就不能改变了,通常我们的做法是:尽量配置一个大的空间,以免不够用,这样做的缺点是比较浪费空间,预估空间不当会引起很多不便. ST ...
- 带你深入理解STL之迭代器和Traits技法
在开始讲迭代器之前,先列举几个例子,由浅入深的来理解一下为什么要设计迭代器. //对于int类的求和函数 int sum(int *a , int n) { int sum = 0 ; for (in ...
- 小白欢乐多——记ssctf的几道题目
小白欢乐多--记ssctf的几道题目 二哥说过来自乌云,回归乌云.Web400来源于此,应当回归于此,有不足的地方欢迎指出. 0x00 Web200 先不急着提web400,让我们先来看看web200 ...
- 在 n 道题目中挑选一些使得所有人对题目的掌握情况不超过一半。
Snark and Philip are preparing the problemset for the upcoming pre-qualification round for semi-quar ...
随机推荐
- SpringBoot入门学习看这一篇就够了
1.SpringBoot是什么? SpringBoot是一套基于Spring框架的微服务框架. 2.为什么需要SpringBoot 由于Spring是一个轻量级的企业开发框架,主要的功能就是用于整合和 ...
- 图像匹配 | NCC 归一化互相关损失 | 代码 + 讲解
文章转载自:微信公众号「机器学习炼丹术」 作者:炼丹兄(已授权) 作者联系方式:微信cyx645016617(欢迎交流共同进步) 本次的内容主要讲解NCCNormalized cross-correl ...
- 2019_西湖论剑_预选赛 testre
2019_西湖论剑_预选赛 testre 程序中关键操作是比较ptr,其中夹杂的一部分v26计算是为了混淆我们的分析.那么我们只要跟踪ptr数组的生成便可,向上发现v11,加密操作数组. 接下来跟踪v ...
- 【故障公告】阿里云 RDS SQL Server 数据库实例 CPU 100% 引发全站故障
非常抱歉,今天 8:48 开始,我们使用的阿里云 RDS SQL Server 数据库实例突然出现 CPU 100% 问题,引发全站故障,由此给您带来麻烦,请您谅解. 发现故障后立即进行主备切换,和 ...
- 【JVM进阶之路】八:性能监控工具-命令行篇
定位问题的时候,知识.经验是关键基础,数据是依据,工具是运用知识处理数据的手段. 在实际的故障排查.性能监控中,常常是操作系统的工具和Java虚拟机的工具结合使用. 1.操作系统工具 1.1.top: ...
- 【死磕JVM】给同事讲了一遍GC后,他要去面试,年轻人,就是容易冲动!
前言 在一个风和日丽的中午,和同事小勇一起走在公司楼下的小公园里面,看到很多的小姐姐,心想什么时候能够和这些小姐姐一起讨论人生呀,美滋滋,嘿嘿嘿. 收起你的哈喇子好不好,小勇总是在这个时候发出声音,挺 ...
- 创建第一个HTML文件
首先右键新建文本文档,然后打开新建的文档,文本内容写上: <html> <head> <title>我的HTML标题</title> </head ...
- 【Python学习笔记】-虚拟环境virtualenv
在开发python应用程序的时候,系统安装的python3只有一个版本:3.4.所有的第三方的包都回被pip安装到python3的site-packages目录下. 如果我们要要同时开发多个应用程序, ...
- 五、python学习-面向对象
1.面对对象程序开发基础(oop) 面对对象:高内聚 低耦合 面向过程: 优点:效率高,执行速度快 缺点:维护性,移植性差,表达不出一类的语义 面向对象: 优点:可读性,可移植性,可维护性高 缺点:执 ...
- 关于js中this的指向详细总结、分析
目录 this的指向详细剖析 当作为函数直接调用时, this => window 当作为构造函数时,this => 构造出的实例对象 当作为对象的方法调用时,this => 调用方 ...