欢迎光临
我的个人博客网站

从几道题目带你深入理解Event Loop_宏队列_微队列

目录

深入探究JavaScript的Event Loop

Javascript是一门单线程语言

但是在运行时难免会遇到需要较长执行时间的任务如: 向后端服务器发送请求。 其他的任务不可能都等它执行完才执行的(同步)否则效率太低了, 于是异步的概念就此产生: 当遇到需要较长时间的任务时将其放入”某个地方”后继续执行其他同步任务, 等所有同步任务执行完毕后再poll(轮询)刚刚这些需要较长时间的任务并得到其结果

而处理异步任务的这一套流程就叫Event Loop即事件循环,是浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制, 于是更完善的说法是: Javascript是一门单线程非阻塞语言

Event Loop的结构

从几道题目带你深入理解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. 只有当所有微任务队列中的任务执行完后, 才能执行宏任务队列中的下一个任务

用流程图表示:

从几道题目带你深入理解Event Loop_宏队列_微队列

通过题目来深入

题目1:

setTimeout(() => {     console.log(1) }, 0) Promise.resolve().then(     () => {         console.log(2)     } ) Promise.resolve().then(     () => {         console.log(4)     } ) console.log(3) 
  1. 执行初始化代码
    从几道题目带你深入理解Event Loop_宏队列_微队列

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

    1. 取出第一个任务到调用栈–打印2, 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行
      从几道题目带你深入理解Event Loop_宏队列_微队列

    2. 取出第二个任务到调用栈–打印4, 执行完后调用栈为空, 微任务队列为空, 第一个宏任务(run script)完成, 可以轮询宏任务队列的下一个任务
      从几道题目带你深入理解Event Loop_宏队列_微队列

    从几道题目带你深入理解Event Loop_宏队列_微队列

  3. 开始轮询执行宏任务队列中的下一个任务
    从几道题目带你深入理解Event Loop_宏队列_微队列

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

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) 
  1. 执行初始化代码
    从几道题目带你深入理解Event Loop_宏队列_微队列

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

    1. 取出第一个任务到调用栈–打印3, 执行完后调用栈为空, 此时第一个then()返回的Promise有了状态、结果, 于是将第二个then()放入微任务队列中, 检查微任务队列是否还有任务有则执行
      从几道题目带你深入理解Event Loop_宏队列_微队列

    从几道题目带你深入理解Event Loop_宏队列_微队列

    1. 调用栈、微任务队列为空, 宏任务run script执行完毕
      从几道题目带你深入理解Event Loop_宏队列_微队列
  3. 开始轮询执行宏任务队列中的下一个任务
    从几道题目带你深入理解Event Loop_宏队列_微队列

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

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. 执行初始化代码
    从几道题目带你深入理解Event Loop_宏队列_微队列

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

    1. 取出第一个任务到调用栈–打印1, 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行
      从几道题目带你深入理解Event Loop_宏队列_微队列

    从几道题目带你深入理解Event Loop_宏队列_微队列

    1. 调用栈、微任务队列为空, 宏任务run script执行完毕
      从几道题目带你深入理解Event Loop_宏队列_微队列
  3. 开始轮询执行宏任务队列中的下一个任务
    从几道题目带你深入理解Event Loop_宏队列_微队列

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

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) ) 
  1. 执行初始化代码
    从几道题目带你深入理解Event Loop_宏队列_微队列

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

    1. 取出第一个任务到调用栈–执行onResolved中的所有代码, 很重要的地方是此时第一个new Promise的第二个then此时会被放入微任务队列中。 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行
      从几道题目带你深入理解Event Loop_宏队列_微队列

    从几道题目带你深入理解Event Loop_宏队列_微队列

    从几道题目带你深入理解Event Loop_宏队列_微队列

    从几道题目带你深入理解Event Loop_宏队列_微队列

    从几道题目带你深入理解Event Loop_宏队列_微队列

    1. 调用栈、微任务队列为空, 宏任务run script执行完毕
      从几道题目带你深入理解Event Loop_宏队列_微队列
  3. 开始轮询执行宏任务队列中的下一个任务
    从几道题目带你深入理解Event Loop_宏队列_微队列

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

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') 
  1. 执行初始化代码
    从几道题目带你深入理解Event Loop_宏队列_微队列

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

    1. 取出第一个任务到调用栈–执行await后的所有代码, 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行
      从几道题目带你深入理解Event Loop_宏队列_微队列

    从几道题目带你深入理解Event Loop_宏队列_微队列

    从几道题目带你深入理解Event Loop_宏队列_微队列

    1. 调用栈、微任务队列为空, 宏任务run script执行完毕
      从几道题目带你深入理解Event Loop_宏队列_微队列
  3. 开始轮询执行宏任务队列中的下一个任务
    从几道题目带你深入理解Event Loop_宏队列_微队列

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

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> 
  1. 执行初始化代码
    从几道题目带你深入理解Event Loop_宏队列_微队列

  2. 初始化代码执行完毕, 调用栈为空所以可以开始轮询执行微任务队列的代码
    从几道题目带你深入理解Event Loop_宏队列_微队列

    1. 取出第一个任务到调用栈–打印promise, 执行完后调用栈为空, 检查微任务队列是否还有任务有则执行
      从几道题目带你深入理解Event Loop_宏队列_微队列

    从几道题目带你深入理解Event Loop_宏队列_微队列

    1. 调用栈、微任务队列为空, 因为存在冒泡, 所以以上操作再进行一次
      从几道题目带你深入理解Event Loop_宏队列_微队列
  3. 宏任务run script执行完毕, 调用栈、微任务队列为空可以轮询执行宏任务队列中的下一个任务
    从几道题目带你深入理解Event Loop_宏队列_微队列

  4. 开始轮询执行宏任务队列中的下一个任务
    从几道题目带你深入理解Event Loop_宏队列_微队列

  5. 微任务队列、调用栈为空, 继续轮询执行宏任务队列中的下一个任务
    从几道题目带你深入理解Event Loop_宏队列_微队列

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

click promise mutate click promise mutate timeout timeout 

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

从几道题目带你深入理解Event Loop_宏队列_微队列

这里令人迷惑的点是: 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不同的地方在于: 终极题1的click是作为回调函数(dispatch), 而这里是直接同步调用的
    从几道题目带你深入理解Event Loop_宏队列_微队列

  2. inner.click执行完毕, inner.click退栈, 由于调用栈并不为空, 所以不能轮询微任务队列, 而是继续执行run script(执行冒泡部分)
    需要注意: 由于outer.click的MutationObserver并未执行所以不会被再次添加进微任务队列中
    从几道题目带你深入理解Event Loop_宏队列_微队列

  3. inner.click退栈, 宏任务run script执行完毕, run script也退栈 调用栈为空, 开始轮询微任务队列
    从几道题目带你深入理解Event Loop_宏队列_微队列

    从几道题目带你深入理解Event Loop_宏队列_微队列

    从几道题目带你深入理解Event Loop_宏队列_微队列

  4. 调用栈、微任务队列为空, 开始轮询执行宏任务队列中的下一个任务
    从几道题目带你深入理解Event Loop_宏队列_微队列

  5. 微任务队列、调用栈为空, 继续轮询执行宏任务队列中的下一个任务
    从几道题目带你深入理解Event Loop_宏队列_微队列

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

click click promise mutate promise timeout timeout 

参考文章:

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

Tasks, microtasks, queues and schedules

赞(0) 打赏
未经允许不得转载:张拓的天空 » 从几道题目带你深入理解Event Loop_宏队列_微队列
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

专业的IT技术经验分享 更专业 更方便

联系我们本站主机

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏