深入探究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)进而可以细分为

  1. 宏任务(macroTasks)

    • script全部代码、
    • setTimeout、
    • setInterval、
    • setImmediate(浏览器暂时不支持,只有IE10支持,具体可见MDN)、
    • I/O、UI Rendering
  2. 微任务(microTasks)

    • Process.nextTick(Node独有)
    • MutationObserver
    • Promise、
    • Object.observe(废弃)

Event Loop的执行顺序

  1. 首先顺序执行初始化代码(run script), 同步代码放入调用栈中执行, 异步代码放入对应的队列中
  2. 所有同步代码执行完毕后,确认调用栈(stack)是否为空, 只有stack为为空才能开始按照队列的特性轮询执行 微任务队列中的代码
  3. 只有当所有微任务队列中的任务执行完后, 才能执行宏任务队列中的下一个任务

用流程图表示:

通过题目来深入

题目1:

  1. setTimeout(() => {
  2. console.log(1)
  3. }, 0)
  4. Promise.resolve().then(
  5. () => {
  6. console.log(2)
  7. }
  8. )
  9. Promise.resolve().then(
  10. () => {
  11. console.log(4)
  12. }
  13. )
  14. console.log(3)
  1. 执行初始化代码

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

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

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

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

于是这道题最终的结果是:

  1. 3 2 4 1

到这需要说明一个东西就是: setTimeout的回调执行是不算在run script中的, 具体原因我并未弄清, 有明白的同学欢迎解释


题目2:

  1. setTimeout(()=>{
  2. console.log(1)
  3. }, 0)
  4. new Promise((resolve, reject) => {
  5. console.log(2)
  6. resolve()
  7. })
  8. .then(
  9. () => {
  10. console.log(3)
  11. }
  12. )
  13. .then(
  14. () => {
  15. console.log(4)
  16. }
  17. )
  18. console.log(5)
  1. 执行初始化代码

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

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

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

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

于是这道题最终的结果是:

  1. 2 5 3 4 1

题目3:

  1. const first = () => {
  2. return new Promise((resolve, reject) => {
  3. console.log(3)
  4. let p = new Promise((resolve, reject) => {
  5. console.log(7)
  6. setTimeout(() => {
  7. console.log(5)
  8. }, 0)
  9. resolve(1)
  10. })
  11. resolve(2)
  12. p.then(
  13. arg => {
  14. console.log(arg)
  15. }
  16. )
  17. })
  18. }
  19. first().then(
  20. arg => {
  21. console.log(arg)
  22. }
  23. )
  24. console.log(4)
  1. 执行初始化代码

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

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

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

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

于是这道题最终的结果是:

  1. 3 7 4 1 2 5

题目4:

  1. setTimeout(()=>{
  2. console.log(0)
  3. }, 0)
  4. new Promise((resolve, reject) => {
  5. console.log(1)
  6. resolve()
  7. })
  8. .then(
  9. () => {
  10. console.log(2)
  11. new Promise((resolve, reject) => {
  12. console.log(3)
  13. resolve()
  14. })
  15. .then(
  16. () => console.log(4)
  17. )
  18. .then(
  19. () => console.log(5)
  20. )
  21. }
  22. )
  23. .then(
  24. () => console.log(6)
  25. )
  26. new Promise((resolve, reject) => {
  27. console.log(7)
  28. resolve()
  29. })
  30. .then(
  31. () => console.log(8)
  32. )
  1. 执行初始化代码

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

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

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

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

于是这道题最终的结果是:

  1. 1 7 2 3 8 4 6 5 0

题目5:

  1. console.log('script start')
  2. async function async1() {
  3. await async2()
  4. console.log('async1 end')
  5. }
  6. async function async2() {
  7. console.log('async2 end')
  8. }
  9. async1()
  10. setTimeout(function () {
  11. console.log('setTimeout')
  12. }, 0)
  13. new Promise(resolve => {
  14. console.log('Promise')
  15. resolve()
  16. })
  17. .then(function () {
  18. console.log('promise1')
  19. })
  20. .then(function () {
  21. console.log('promise2')
  22. })
  23. console.log('script end')
  1. 执行初始化代码

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

    1. 取出第一个任务到调用栈--执行await后的所有代码, 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行

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

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

于是这道题最终的结果是:

  1. script start
  2. async2 end
  3. Promise
  4. script end
  5. async1 end
  6. promise1
  7. promise2
  8. setTimeout

终极题1:

  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <style>
  8. .outer {
  9. width: 200px;
  10. height: 200px;
  11. background-color: orange;
  12. }
  13. .inner {
  14. width: 100px;
  15. height: 100px;
  16. background-color: salmon;
  17. }
  18. </style>
  19. </head>
  20. <body>
  21. <div class="outer">
  22. <div class="inner"></div>
  23. </div>
  24. <script>
  25. var outer = document.querySelector('.outer')
  26. var inner = document.querySelector('.inner')
  27. new MutationObserver(function () {
  28. console.log('mutate')
  29. }).observe(outer, {
  30. attributes: true,
  31. })
  32. function onClick() {
  33. console.log('click')
  34. setTimeout(function () {
  35. console.log('timeout')
  36. }, 0)
  37. Promise.resolve().then(function () {
  38. console.log('promise')
  39. })
  40. outer.setAttribute('data-random', Math.random())
  41. }
  42. inner.addEventListener('click', onClick)
  43. outer.addEventListener('click', onClick)
  44. </script>
  45. </body>
  46. </html>
  1. 执行初始化代码

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

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

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

  3. 宏任务run script执行完毕, 调用栈、微任务队列为空可以轮询执行宏任务队列中的下一个任务

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

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

于是这道题最终的结果是:

  1. click
  2. promise
  3. mutate
  4. click
  5. promise
  6. mutate
  7. timeout
  8. timeout

不同浏览器下的不同结果(如果你的结果在这其中, 也是对的)

这里令人迷惑的点是: outer的冒泡执行为什么比outer的setTimeout先

那是因为:

  • 首先outer的setTimeout是一个宏任务, 它进入宏任务队列时是在了run script的后面
  • inner执行到mutate后run script并没有执行完, 而是还有一个outer.click的冒泡要执行
  • 只有执行完该冒泡后, run script才真正执行完(才可以执行下一个宏任务)

终极题2:

  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <style>
  8. .outer {
  9. width: 200px;
  10. height: 200px;
  11. background-color: orange;
  12. }
  13. .inner {
  14. width: 100px;
  15. height: 100px;
  16. background-color: salmon;
  17. }
  18. </style>
  19. </head>
  20. <body>
  21. <div class="outer">
  22. <div class="inner"></div>
  23. </div>
  24. <script>
  25. var outer = document.querySelector('.outer')
  26. var inner = document.querySelector('.inner')
  27. new MutationObserver(function () {
  28. console.log('mutate')
  29. }).observe(outer, {
  30. attributes: true,
  31. })
  32. function onClick() {
  33. console.log('click')
  34. setTimeout(function () {
  35. console.log('timeout')
  36. }, 0)
  37. Promise.resolve().then(function () {
  38. console.log('promise')
  39. })
  40. outer.setAttribute('data-random', Math.random())
  41. }
  42. inner.addEventListener('click', onClick)
  43. outer.addEventListener('click', onClick)
  44. inner.click() // 模拟点击inner
  45. </script>
  46. </body>
  47. </html>
  1. 执行初始化代码, 这里与终极题1不同的地方在于: 终极题1的click是作为回调函数(dispatch), 而这里是直接同步调用的

  2. inner.click执行完毕, inner.click退栈, 由于调用栈并不为空, 所以不能轮询微任务队列, 而是继续执行run script(执行冒泡部分)

    需要注意: 由于outer.click的MutationObserver并未执行所以不会被再次添加进微任务队列中

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

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

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

于是这道题最终的结果是:

  1. click
  2. click
  3. promise
  4. mutate
  5. promise
  6. timeout
  7. timeout

参考文章:

一次弄懂Event Loop(彻底解决此类面试问题)

Tasks, microtasks, queues and schedules

从几道题目带你深入理解Event Loop_宏队列_微队列的更多相关文章

  1. hdu 动态规划(46道题目)倾情奉献~ 【只提供思路与状态转移方程】(转)

    HDU 动态规划(46道题目)倾情奉献~ [只提供思路与状态转移方程] Robberies http://acm.hdu.edu.cn/showproblem.php?pid=2955      背包 ...

  2. Java基础 带你深刻理解自动装箱,拆箱含义

    1.什么是装箱,什么是拆箱装箱:把基本数据类型转换为包装类.拆箱:把包装类转换为基本数据类型.基本数据类型所对应的包装类:int(几个字节4)- Integerbyte(1)- Byteshort(2 ...

  3. C语言超级经典400道题目

    C语言超级经典400道题目 1.C语言程序的基本单位是____ A) 程序行 B) 语句 C) 函数 D) 字符.C.1 2.C语言程序的三种基本结构是____构A.顺序结构,选择结构,循环结 B.递 ...

  4. 带你深入理解STL之Set和Map

    在上一篇博客带你深入理解STL之RBTree中,讲到了STL中关于红黑树的实现,理解起来比较复杂,正所谓前人种树,后人乘凉,RBTree把树都种好了,接下来就该set和map这类关联式容器来" ...

  5. 带你深入理解STL之Stack和Queue

    上一篇博客,带你深入理解STL之Deque容器中详细介绍了deque容器的源码实现方式.结合前面介绍的两个容器vector和list,在使用的过程中,我们确实要知道在什么情况下需要选择恰当的容器来满足 ...

  6. 带你深入理解STL之Vector容器

    C++内置了数组的类型,在使用数组的时候,必须指定数组的长度,一旦配置了就不能改变了,通常我们的做法是:尽量配置一个大的空间,以免不够用,这样做的缺点是比较浪费空间,预估空间不当会引起很多不便. ST ...

  7. 带你深入理解STL之迭代器和Traits技法

    在开始讲迭代器之前,先列举几个例子,由浅入深的来理解一下为什么要设计迭代器. //对于int类的求和函数 int sum(int *a , int n) { int sum = 0 ; for (in ...

  8. 小白欢乐多——记ssctf的几道题目

    小白欢乐多--记ssctf的几道题目 二哥说过来自乌云,回归乌云.Web400来源于此,应当回归于此,有不足的地方欢迎指出. 0x00 Web200 先不急着提web400,让我们先来看看web200 ...

  9. 在 n 道题目中挑选一些使得所有人对题目的掌握情况不超过一半。

    Snark and Philip are preparing the problemset for the upcoming pre-qualification round for semi-quar ...

随机推荐

  1. 如何学习python爬虫

    分享网易云课堂上一个不错的视频教学:http://study.163.com/course/courseMain.htm?courseId=1003285002

  2. Django之模版层

    一.模版简介 你可能已经注意到我们在例子视图中返回文本的方式有点特别,也就是说,HTML被直接硬编码在python代码之中. def current_datetime(request): now = ...

  3. P1036_选数(JAVA语言)

    题目描述 已知 n 个整数x1​,x2​,-,xn​,以及1个整数k(k<n).从n个整数中任选k个整数相加,可分别得到一系列的和.例如当n=4,k=3,4个整数分别为3,7,12,19时,可得 ...

  4. WERTYU_键盘错位(JAVA语言)

    package 第三章; import java.util.Scanner; /*  *  把手放在键盘上时,稍不注意就会往右错一位.这样,输入Q会变成输入W,输入J会变成输入K等.        输 ...

  5. sqli-labs系列——第五关

    less5 更改id后无果,不能用union联合查询 此处用报错注入 报错注入的概念:(1). 通过floor报错 and (select 1 from (select count(*),concat ...

  6. 庐山真面目之十二微服务架构基于Docker搭建Consul集群、Ocelot网关集群和IdentityServer版本实现

    庐山真面目之十二微服务架构基于Docker搭建Consul集群.Ocelot网关集群和IdentityServer版本实现 一.简介      在第七篇文章<庐山真面目之七微服务架构Consul ...

  7. [^ ] 跟 [! ] 差在哪?-- Shell十三问<第十四问>

    [^ ] 跟 [! ] 差在哪?-- Shell十三问<第十四问> 这道题目说穿了, 就是要探讨 Wildcard(通配符)与 Regular Expression(正则表达式)的差别的. ...

  8. java面试-JDK自带的JVM 监控和性能分析工具用过哪些?

    一.JDK的命令行工具 1.jps(JVM Process Status Tools):虚拟机进程状况工具 jps -l 2.jinfo(Configuration Info for java):Ja ...

  9. C语言-字符串函数的实现(二)之strcpy

    C语言中的字符串函数有如下这些 获取字符串长度 strlen 长度不受限制的字符串函数 strcpy strcat strcmp 长度受限制的字符串函数 strncpy strncat strncmp ...

  10. Day06_30_抽象类(Abstract)

    抽象类 Abstract 什么是抽象类? 在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就 ...