js异步——事件循环和消息队列

发布时间 2023-04-09 14:36:28作者: 菲尼克斯交警

前言

上篇文章中介绍了多进程的浏览器基本架构,现在,我们来谈谈单线程的 JS 代码、消息队列、事件循环、微任务和宏任务。

单线程的 JavaScript

什么是单线程 js?

如果你已经仔细阅读过上一篇文章,那么答案是显而易见的:由于浏览器是由渲染进程的主线程来执行 js 代码的,换句话说,js的运行位置是渲染进程的主线程,所以 js 自然而然就是单线程的。

js 为什么设计成单线程的?

这个问题的答案同样在上一篇文章中有所体现。浏览器中的js执行和页面渲染是在同一个线程中发生的,主线程在解析HTML生成DOM树的过程中,如果遇到<script>标签会先执行js代码而阻塞对HTML的解析,因为 js 能够修改DOM进而影响渲染结果。但如果 js 可以拥有多个线程来执行,那么会出现一边解析HTML进行渲染,一边执行的 js 代码操作 DOM ,这样会影响到页面最终渲染效果的一致性(可预见性)。

同步任务和异步任务

  • 同步任务:按顺序执行的js代码,上一个任务结束才能执行下一个任务,主线程中只执行同步任务
  • 异步任务:不进入主线程执行,而是由宿主环境提供的线程执行。当异步任务完成时,会在消息队列中添加异步任务的回调函数。

消息队列

此时,你可能会有疑问:既然 JS 是单线程的,而异步任务又不是在主线程中执行的,这不是矛盾了吗?实际上,JS的确是单线程,但他的宿主环境(浏览器,Node.js)可不是单线程的,js中一些耗时的任务,可以交由宿主环境的其他线程来执行,但这与多线程语言可以开启多个线程并行执行任务并不相同。

让我们来看看异步任务执行时发生了什么。假设js代码发出了一个异步 http 请求,此时由IO线程来接管执行http请求的代码,主线程将异步任务挂起,并继续执行接下来的同步代码,当IO线程接收到了服务器发来的响应,便将异步任务的回调加入到消息队列的队尾。

消息队列(任务队列)是在主线程之外的数据结构,每当有异步任务完成,那么他的回调函数(callback)就会被push到消息队列的队尾。主线程中所有同步任务执行完之后,由事件循环来通知主线程开始执行消息队列中的任务。

事件循环(Event Loop)

简单的说,事件循环起到通知主线程该执行异步任务回调的作用。每当主线程的同步代码执行完毕后,浏览器变开始进行事件循环,让消息队列的中的队首元素出队,并在主线程中执行,执行完成后,事件循环再次查询消息队列中的队首元素......

事件循环和消息队列相互配合,管理异步任务和它的回调函数。

微任务和宏任务

确切地说,消息队列中的元素是一个一个地宏任务,而宏任务内部有一个微任务队列。js主线程是第一个宏任务。

  • 常见的微任务有:Promise.then(),await 发出的消息,Object.observeprocess.nextTick

  • 常见的宏任务有:主线程所有同步任务、setTimeout的回调、setInterval的回调、setImmediate的回调

    w3c规定:setTimeout() 有一个默认的最小延时时间为4ms,所以即使参数为0,那也是4ms后会将回调函数添加到消息队列

每当一个宏任务的主要任务完成后,事件循环便开始捕获其微任务队列中的微任务,当这个宏任务内的微任务队列为空时,事件循环才开始捕获下一个宏任务和它的微任务队列.....

为什么要分宏任务和微任务?这样的设计是为了给紧急任务一个“插队”的机会,否则新进队列的任务永远放在队尾。可以把微任务理解为更加着急执行的任务,所以可以“插队”,排在宏任务之前被事件循环捕捉。

下面用一组图片来形象地展示消息队列和事件循环、异步任务的运行机制:

没有异步任务时,主线程的一次执行

在主线程中引入事件循环

渲染进程的线程之间发送通知

线程模型:消息队列、事件循环和跨进程发送信息

参考

阿里一面:熟悉事件循环?那谈谈为什么会分为宏任务和微任务。

浅谈浏览器架构、单线程js、事件循环、消息队列、宏任务和微任务