js中消息队列和事件循环到底是怎么个事,宏任务和微任务还存在吗?

为什么需要消息队列和时间循环呢?我们从开头说,是一个很长的故事。

1. 渲染进程

首先浏览器里有多个进程,每个进程中又包含许多线程,详请参考这篇文章。其中负责处理DOM、解析HTML和CSS、计算样式、JS任务、IO事件、绘制页面等任务的渲染主线程(渲染进程中)非常的繁忙。如果想让这么多不同类型的任务有条不紊的调度和执行,需要消息队列和事件循环机制。

1.1 单线程处理安排好的任务

如果只执行一段js代码,把js任务写进渲染主线程中,任务执行完成后,线程会自动退出。这是最简单的情况。

1.2 单线程运行过程中处理新任务

但是并不是所有的任务都是在执行之前统一安排好的,一般在主线程运行过程中,会有新的任务不断产生。所以需要接收并执行新的任务,这个时候就需要采用事件循环机制。在渲染主线程中使用一个无限的for循环监听是否有新的任务。

1.3 处理其他线程的任务

在1.2中监听的任务都来自于线程内部,如果其他线程想要执行任务,例如:渲染主线程会频繁接受来自于IO线程的一些资源加载完成事件、鼠标点击事件等任务。通用模式是使用消息队列

消息队列如上图所示,其是一种数据结构,先进先出。从队尾添加新任务,从队首取出任务执行。

此时线程模型大概为: 渲染主线程无限循环读取并执行消息队列中的任务,其他线程会将任务添加到消息队列中。

1.4 处理其他进程的任务

线程之间的消息队列已经实现了,如果从网络进程或者浏览器进程中的任务想要执行怎么办呢?

跨进程之间的任务在实际情景中也是频繁发生的,通过IPC机制渲染进程使用IO线程专门接受从其他进程传过来的消息。此时网络进程通过IPC机制传输HTML文档数据,渲染引擎会将"解析DOM"任务添加到消息队列中。

2. 定时器任务的执行

消息队列中接收了各种类型的消息,如页面渲染、输入事件、微任务、定时器等,这些任务都会在主线程中执行。如果说直接将定时器任务加入任务队列,渲染主线程在执行时会等待相应的延迟时间,可能会长期处于阻塞状态,"卡死"页面,无法响应用户的操作。Chrome为定时器和一些需要延迟的任务做了特殊处理。

2.1 延迟队列

定时器和一些需要延迟的任务,Chrome为其维护了一个延迟的"队列"。加引号是因为此队列不是数据结构意义上的队列。不是先进先出,而是有延迟任务延迟时间到了之后在添加到消息队列中排队执行。

js 复制代码
let id = setTimeout(timeDelayFn, 2000);

function timeDelayFn() {
    console.log('time-delay')
}

当js通过setTimeout调用setTimeout设置回调函数时,渲染进程会创建一个回调任务,包含了回调函数timeDelayFn、当前发起时间和延迟时间。接着将该回调任务添加到延迟队列中。渲染主进程则继续读取消息队列中的下一个任务。

2.2 执行时机

延迟队列通过发起时间和延迟时间计算,将到期的任务添加到消息队列中排队执行。

因此,在很多情况下,setTimeout回调函数的执行时间会长于设置的延时时间。

js 复制代码
function timeDelayFn() {
    setTimeout(() => {
        console.log(123)
    }, 0);
    for (let index = 0; index < 10000; index++) {
        console.log(index)
    }
}

timeDelayFn()
  • 在这段代码在执行时,第二行的setTimeout的回调函数会被放到延迟队列中。
  • 接着开始执行for循环(长任务),虽然在执行的过程中,setTimeout延时时间结束,但是此时并不会执行,而是被添加到消息队列中,需要等待for循环执行结束后再去执行其回调函数。 for循环要执行一万次,执行时间若为1.6s,此setTimeout设置的回调函数会在1.6s之后再执行,虽然设置的延迟时间为0s。

2.3 异步任务

像延迟任务、fetch请求和文件文件操作等都属于异步任务

回调函数大家都知道,是一个函数作为参数传递给了另一个函数,那么作为参数传递的这个函数即为回调函数

js 复制代码
function t(){
    console.log(123)
}

function testTimeoutFn(t) {
    t()
    for (let index = 0; index < 10000; index++) {
        console.log(index)
    }
}

testTimeoutFn(t)

上述代码中的t即为回调函数。t函数是在testTimeoutFn函数执行完成之前执行的,此为同步回调

js 复制代码
function timeDelayFn() {
    console.log(123)
}

function testTimeoutFn(cb) {
    setTimeout(cb, 2000);
    for (let index = 0; index < 10; index++) {
        console.log(index)
    }
}

testTimeoutFn(timeDelayFn)

如果让回调函数在testTimeoutFn函数执行完成后在执行,并没有在testTimeoutFn函数内部被调用,此为异步回调

这些异步任务的核心目标就是:不阻塞渲染主线程的执行,同时保证任务的正确和有序执行。因此对于这类异步任务,执行的过程为:

  1. 渲染主线程遇到异步任务(setTimeout、fetch等),将其交给对应的模块(定时器、网络)。
  2. 异步任务达到触发条件后,将回调函数添加到消息队列中排队执行。

3. 微任务和宏任务

异步任务解决了单个任务执行时间过长阻塞线程的问题,对于渲染主线程来说,单线程执行任务,还有另外一个问题,即碰到优先级较高的任务怎么办?比如页面DOM变化,用户想要尽快看到页面响应,如果还是添加到消息队列的尾部,可能前面会有很多的任务在排队,会影响页面响应。此时微任务就诞生了。

3.1 宏任务

在消息队列中任务称为宏任务,上述1和2中所讲的事件循环机制即为宏任务的循环机制。在每个宏任务执行的时候,都会为该任务维护一个系统调用栈,类似于js的调用栈。

常见的宏任务有: 定时任务:setTimeout、setInterval 动画任务:requestAnimationFrame 常规任务:整体js代码、用户交互事件、页面渲染 文件任务:I/O读写

3.2 微任务

上文讲述的异步回调,其执行方式有两种:

  1. 把异步函数封装成宏任务,添加到消息队列尾部排队执行;
  2. 维护为微任务,在主函数执行完成之后,当前宏任务结束之前执行。

例如,在执行一段js脚本时,浏览器会为其创建一个全局执行上下文,同时创建一个微任务队列,存放在执行js脚本时产生的微任务。也就是说,每个宏任务都关联了一个微任务队列。

常见的微任务有:

  • Promise相关:.then/catch/finally
  • DOM监听:MutationObserver
  • API支持:queueMicrotask

当这些微任务添加到微任务队列后,其执行时机是在当前宏任务中的js快执行完成,js引擎准备退出全局执行上下文并清空调用栈时,按照顺序执行微任务队列中的微任务。如果在执行微任务队列中的微任务时,产生了新的微任务,则立即添加到微任务队列中,不会推迟到下个宏任务中执行。

3.3 举例

举个例子,在网络进程接收到HTML文件后,交给渲染主线程解析的宏任务,在执行过程中,遇到了js脚本:

js 复制代码
const dom = document.querySelector(".btn")
const divDom = document.createElement("div")
divDom.innerHTML = 'lz'
dom.appendChild(divDom)

new Promise((resolve)=>{resolve(1)})
    .then(res=>{})

产生部分微任务进入微任务队列:

  1. 因为在解析HTML过程中遇到了js脚本,则停止HTML解析,进入js执行环境执行js脚本。
  2. 脚本通过appendChild和promise创建了微任务,加入到微任务队列中。
  3. 随着js执行结束,即将退出全局执行上下文时,此时js引擎检查微任务队列并依次执行。
  4. 微任务执行完成,队列清空后,退出全局执行上下文。

微任务的产生,解决了单线程中优先级较高的任务产生后不能及时执行的问题。

因此网络上很多题目就可以做了,先进一个宏任务,接着执行js脚本,最后是微任务。

4. 目前的宏任务

我看了几篇文章,说W3C不再使用宏队列的说法,而是细分为微队列和其他队列。

微队列的优先级最高,优先于其他所有任务执行。目前我看代码的执行结果顺序是没变的,而且按照其描述,执行顺序也是不变的,所以应该是提供一个更新的概念,了解一下先。

参考文献:juejin.cn/post/743845...

juejin.cn/post/735097...

blog.csdn.net/weixin_4428...

juejin.cn/post/684490...

深入:微任务与 Javascript 运行时环境 - Web API | MDN

time.geekbang.org/column/intr...

相关推荐
小小小小宇13 分钟前
前端小tips
前端
小小小小宇22 分钟前
二维数组按顺时针螺旋顺序
前端
安木夕41 分钟前
C#-Visual Studio宇宙第一IDE使用实践
前端·c#·.net
努力敲代码呀~43 分钟前
前端高频面试题2:浏览器/计算机网络
前端·计算机网络·html
高山我梦口香糖1 小时前
[electron]预脚本不显示内联script
前端·javascript·electron
神探小白牙1 小时前
vue-video-player视频保活成功确无法推送问题
前端·vue.js·音视频
402 Payment Required1 小时前
serv00 ssh登录保活脚本-邮件通知版
运维·chrome·ssh
Angel_girl3192 小时前
vue项目使用svg图标
前端·vue.js
難釋懷2 小时前
vue 项目中常用的 2 个 Ajax 库
前端·vue.js·ajax
Qian Xiaoo2 小时前
Ajax入门
前端·ajax·okhttp