JS异步大法总结篇(一):消息队列和事件循环机制,站在山顶看事件机制

269 阅读7分钟

前言: Javcscript是单线程机制,单线程模型指的是,JavaScript只在一个线程上运行。单线程的最大缺点就是有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。所以慢慢就有了异步编程的概念,引入异步编程来解决单线程机制所带来的问题。JS 异步编程发展史:callback->promise->generator->async/await。 JS异步编程现已经成为JS编程中非常重要的内容,所以深入理解异步编程十分有必要。

本章节内容会阐述任务执行机制,包括消息队列、事件循环、宏任务与微任务等基本概念。接着会深入探究JS异步发展各个解决办法以及优缺点。

本小节主要阐述任务执行机制的基本知识点,从底层出发揭开JS异步编程的面纱。阅读时长约15min。

整章节思维导图:

思维导图

1.任务执行机制

图1:任务执行机制图示
上图有以下三个要点:

1.1 事件循环机制

  • 本质是for 循环语句,通过for循环主线程会一直循环执行
  • 主线程从消息队列中取出任务,然后执行任务

1.2 消息队列

  • 消息队列是用于存放任务的数据结构,就有“先进先出的特点”
  • IO线程中产生任务添加至队尾
  • 任务类型:输入事件(鼠标滚动、点击、移动)、文件读写、WebSocket、JavaScript 定时器等等

1.3 跨进程通信

  • IO 线程用来接收其他进程传进来的消息

2.页面单线程的缺点

2.1 处理优先级问题

比如DOM发生了临时的变化,若采用同步的方式进行通知,单个任务执行事件过长则会导致执行效率的问题

如果将这些 DOM 变化做成异步的消息事件,添加到消息队列的尾部,那么又会影响到监控的实时性,因为在添加到消息队列的过程中,可能前面就有很多任务在排队了。

如何处理处理执行效率和实时性的问题呢?因此我们引入了微任务。

通常我们把消息队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列,在执行宏任务的过程中,如果 DOM 有变化,那么就会将该变化添加到微任务列表中,这样就不会影响到宏任务的继续执行,因此也就解决了执行效率的问题。

等宏任务中的主要功能都直接完成之后(此时宏任务还未结束),这时候,渲染引擎并不着急去执行下一个宏任务,而是执行当前宏任务中的微任务,因为DOM变化的事件都保存在这些微任务队列中,这样也就解决了实时性问题。

对于宏任务与微任务稍后做出详细解释。

2.2 单任务执行时间过长的问题

因为所有的任务都是在单线程中执行的,所以每次只能执行一个任务,而其他任务就都处于等待状态。 对这种情况,JavaScript 可以通过回调功能来规避这种问题,也就是让要执行的 JavaScript 任务滞后执行。

3.同步回调与异步回调

3.1什么是回调函数(callback function)

回调函数就是,将一个函数作为参数传递给另一个函数,作为参数的这个函数就叫做回调函数,举个栗子:

function callback() {
    console.log('我是一个回调函数')
}
function main(cb){
}
main(callback) //callback作为main的回调函数

3.2同步回调and异步回调

同步回调:回调函数 callback 是在主函数 main 返回之前执行的,我们把这个回调过程称为同步回调,还是以上面的为例:

function main(cb) {
    console.log('main函数开始执行')
    cb() //callback在main函数执行之前结束了
    console.log('main函数执行完毕')
}

异步回调:与同步相反,同步是之前,异步是之后

function main(cb) {
    console.log('main函数开始执行')
    setTimeout(cb, 0) //callback在main函数执行之后才结束
    console.log('main函数执行完毕')
}

理解了同步回调与异步回调对后面知识的理解会有很大帮助。

4.宏任务与微任务

4.1宏任务

消息队列
如图所示,消息队列中的任务就是宏任务。 宏任务的任务类型通常有:

  • 渲染事件(如解析 DOM、计算布局、绘制);
  • 用户交互事件(如鼠标点击、滚动页面、放大缩小等);
  • JavaScript 脚本执行事件;
  • 网络请求完成、文件读写完成事件。

IO线程中产生任务添加至队尾,然后主线程从消息队列的队头中取出任务执行。 值得注意的一点是:每个宏任务都绑定了一个微任务

4.2微任务

微任务位于宏任务队列中,微任务就像是工厂里每个部门(每个宏任务)找的临时工,用来处理一些临时的工作。当然临时工处理工作事物是要在部门工作基本结束时(可以理解黑心老板压榨临时工)。微任务有两个很重要的问题: 第一个就是:如何产生微任务呢?

  • 使用 MutationObserver 监控某个DOM节点,节点发生变化时。即当DOM临时发生改变时,会将此任务添加至当 前宏任务的微任务队列中。
  • 使用 Promise,当调用 Promise.resolve()或者Promise.reject()的时候,也会将resolve成功返回的内容或者是reject失败返回的内容添加到当前宏任务的微任务队列中。Promise异步会在下一章详细解释。

第二个就是:何时执行微任务队列中的微任务呢? 当宏任务中的同步代码执行完成,会到一个检查点,之后JavaScript引擎会检查微任务队列。发现微任务存在微任务则执行队列里的微任务,否则当前宏任务执行完后退出,执行下一个宏任务。

5.setTimeout里的延迟队列

:在 Chrome 中除了正常使用的消息队列之外,还有另外一个消息队列,这个队列中维护了需要延迟执行的任务列表,包括了定时器和 Chromium内部一些需要延迟执行的任务。所以当通过JavaScript创建一个定时器时,渲染进程会将该定时器的回调任务添加到延迟队列中。 任务优先级:消息队列中的宏任务->微任务队列中的任务->延迟队列中的任务

function showName() {
    //setTimeout()函数中的代码会放置在延迟队列中。
    setTimeout(() => {
        console.log('要等待消息队列、微任务队列里的任务执行玩,我再执行')
    }, 0)
    console.log('我执行完毕了,接下来执行延迟队列里的代码')
}
showName()

总结: 本文从任务执行机制出发,引入了消息队列与事件循环的概念。之后通过页面单线程的缺点引出了微任务。之后详细解释了宏任务与微任务,宏任务就是消息队列中的任务,微任务就是宏任务中微任务队列里的任务。最后讲解了定时器setTimeout()中的延迟队列。

有了这些知识我们后面理解其Peomise、async/await等知识就轻松多了!下一章中将会深入了解Promise。

参考文章: 宏任务和微任务:不是所有任务都是一个待遇

本人前端菜鸟一枚,可能有一些对方总结不到位或者存在问题,欢迎大家批评斧正,也希望与大家一起交流学习,共同进步。