宏任务和微任务的前世今生:从一道面试题说起

做道题目

大家可能见过这种面试题目,根据对于宏任务和微任务的执行时机的理解,列出下面的输出顺序。

   console.log('输出1')
   setTimeout(()=>{
      console.log("输出2")
   })
   console.log('输出3')
   const promise1 = new Promise((resolve)=>{
       console.log('输出4')
       resolve(1)
   })
   promise1.then(()=>{ 
        console.log("输出5") 
        setTimeout(()=>{
         console.log("输出6")
        })
   })

相信能够很快给出答案,能够从宏任务和微任务的执行时机,来说明输出的顺序:程序顺序执行输出1,遇到setTimeout放到延迟队列,顺序执行3,Promise会先执行构造函数,输出4,执行微任务输出5,setTimeout放到入延迟队列,延迟队列按照先后顺序,输出2,输出6。

输出1
输出3
输出4
输出5
输出2
输出6

我们再进一步思考🤔?

为什么会有宏任务和微任务的概念,它们被设计出来是为了解决什么问题,底层原理是什么。知其然,也知其所以然,才能更系统地掌握宏任务和微任务。

宏任务和微任务

消息队列

在谈宏任务和微任务之前,先来谈谈消息队列。

消息队列是一种先进先出的数据结构,被用来存放要执行的任务。

在浏览器中,渲染主线程会收到其他线程发送的消息,例如IO线程发过来的网络资源加载完成(消息队列中的任务是有多种类型的,例如文件读写、输入事件、定时器等等),会把这些任务添加到消息队列中,然后主线程执行任务时通过事件循环机制,会从消息队列中一个一个读取执行。

image.png

但使用这种方式会有一个问题

由于消息队列是按照FIFO顺序执行,但是如果有一个高优先级的任务(例如DOM变化、UI更新等等),那就只能等待前面队列中的事件执行完毕,这时候执行的时间并不能被精确控制,可能受其他任务的影响,导致执行时间比预计的时间更长。对于比较要求实时的场景,这种时间间隔太长,用户很容易就感觉到卡顿或者延迟。

image.png

处理高优先级的任务

那么如何处理高优先级的任务呢?可能有两种思路:

  • 思路 1

使用订阅者模式,像DOM一有变化,就马上调用订阅的事件,将事件添加到任务队列,但由于DOM变化触发会很高频,导致被添加很多个相同的任务到任务队列,导致队列的执行时间会变长,造成执行效率的下降。

  • 思路 2

既然思路1会导致添加多次任务,那如果把事件转换为异步事件,刚好可以解决这个问题。但这种方式带来另外一个问题,由于消息队列中可能存在很多任务,需要等待前面的事件执行完成之后,才会被执行,时间相对比较久,影响到响应的实时性。

所以在效率实时性之间,需要做一个权衡。这时候就引入了微任务的概念。

微任务是怎么平衡效率和实时性?

引入微任务之后,相对应的消息队列中的任务称为宏任务。每个宏任务中都会包含一个微任务队列。

执行宏任务的过程中,例如DOM产生变化,会将该事件添加到该宏任务的微任务队列中,不会影响当前宏任务的执行,这样就解决了执行效率的问题。

对于实时性,在当前宏任务完成之后,会执行该宏任务中的微任务队列的任务,等该微任务队列执行完成之后,才会继续执行下一个宏任务,这就解决了实时性的问题。

image.png

宏任务和微任务区别

宏任务

常见的宏任务:

  • 渲染事件(如解析 DOM、样式、布局、绘制)
  • DOM事件(例如点击事件、键盘事件、鼠标事件等)
  • JS定时器
  • 网络请求完成等。

微任务

在浏览器里,微任务产生的方式有两种:

  • Promise中调用resolve() 和 reject() 会产生微任务,并将对应的任务按照顺序添加到当前宏任务的微任务队列上
  • 使用 MutationObserver API 观察 DOM 变化时,其回调也会被添加到微任务队列中。

区别

宏任务和微任务本质上就是异步回调,但是各自执行时机不同。

  • 宏任务:是添加到消息队列,当事件循环到这个任务的时候就会执行回调
  • 微任务是添加到微任务队列,是在主函数执行结束之后,当前宏任务结束之前执行回调
全部评论

相关推荐

饼子吃到撑:当我看到外企的时候,我就知道这大概率可能是真的
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务