宏任务与微任务
JavaScript运行机制:JavaScript是单线程的,它运行的环境般为浏览器或者Node,单线程同一个时间只能做一件事,在JavaScript运行的时候,主线程会形成一个栈(调用栈/执行栈),这个存储函数调用的栈结构遵循先进后出的原则。
任务执行模式:同步模式(Synchronous)和异步模式(Asynchronous)
JavaScript
单线程的执行方式会存在一些问题,就是一些高耗时操作会带来进程阻塞;如果当一个语句也需要执行很长时间的话,比如请求数据、定时器、读取文件等等,后面的任务一直等待,为了解决这个问题JavaScript
中出现了同步任务和异步任务。
同步任务
进入调用栈中在主线程上排队按照代码顺序和调用顺序执行的任务(只有前一个任务执行完毕才能执行后一个任务)形成一个执行栈,执行结束后就移出执行栈;
异步任务
进入调用栈中,然后发起调用,但其响应回调任务不进入主线程,而是进入任务队列当主线程中的任务执行完毕(即所有同步任务结束后),就从任务队列中取出任务放进主线程中来进行执行。由于主线程不断重复的获得任务、执行任务、再获取再执行,所以这种机制被叫做事件循环(Event Loop)。
异步任务不是直接进入任务队列的,也就是被调用且发起请求是前提,响应和处理数据的函数进入任务队列。
宏任务和微任务
ES6 规范中,microtask 称为 jobs,macrotask 称为 task
宏任务是由宿主发起的,而微任务由JavaScript自身发起。
根据任务执行机制,同步任务的执行栈是栈结构(先进后出),而异步任务的任务队列是队列结构(先进先出),两种模式都是有顺序的,可是异步任务难免出现非常耗时的请求,这就使得按顺序执行不合适,因此浏览器会将异步任务分为宏任务和微任务,也就是不同的任务会有不同的执行优先级。
浏览器线程
虽然JavaScript
是单线程语言,但是浏览器不是单线程的,不同的线程就会对不同的事件进行处理,当对应事件可以执行的时候,对应线程就会将其放入任务队列。 浏览器线程有js引擎线程、GUI渲染线程、http异步网络请求线程、定时触发器线程、浏览器事件处理线程等。其中:
- http异步网络请求线程:处理用户的get、post等请求,等返回结果后将回调函数推入到任务队列;
- 定时触发器线程:
setInterval
、setTimeout
等待时间结束后,会把执行函数推入任务队列;
常见宏任务(Macrotask)
script
setTimeout
setInterval
I/O
requestAnimationFrame
UI交互事件
常见微任务(Microtask)
Promise.then(catch/finally)
async/await
- async/await本质上还是基于Promise的一些封装,而Promise是属于微任务的一种。所以在使用await关键字与Promise.then相同;
- sync函数在await之前的代码都是同步执行的,可以理解为await之前的代码属于new Promise时传入的代码,await之后的所有代码都是在Promise.then中的回调;
- await之后的代码必须等await语句执行完成后(包括微任务完成),才能执行后面的,也就是说,只有运行完await语句,才把await语句后面的全部代码加入到微任务行列,所以,在遇到await promise时,必须等await promise函数执行完毕才能对await语句后面的全部代码加入到微任务中;
事件执行
总体:
主线程 -->线程上的宏任务队列1 -->宏任务队列1中创建的微任务 -->线程上的宏任务队列2-->宏任务队列2中创建的微任务......
事件循环(Event Loop)
事件循环的具体流程:
第一次执行的时候,整体代码
script
会放入宏任务,所以事件循环第一个宏任务开始的;微任务队列没清空之前,是不会执行下一个宏任务的。
- 从宏任务队列中,按照入队顺序,把第一个宏任务放入调用栈,开始执行;
- 执行完该宏任务下所有同步任务后(即调用栈清空后)该宏任务被推出宏任务队列,然后微任务队列开始按照入队顺序,依次执行其中的微任务,直至微任务队列清空为止;
- 当微任务队列清空后,一个事件循环结束;
- 接着从宏任务队列中,找到下一个执行的宏任务,开始第二个事件循环,直至宏任务队列清空为止。
示例
来更新一个有关js事件执行的输出题可以加深理解(如果是练习可以结合断点调试,可以非常清楚的看到执行的顺序)
题目
const myPromise = Promise.resolve(Promise.resolve("Promise!"));
function func1() {
myPromise.then(res => {
console.log("执行第一个then", res)
return res
}).then(res => {
console.log('执行第二个then', res)
});
setTimeout(() => console.log("Timeout!"), 0);
console.log("Last line!");
}
async function func2() {
console.log('进入func2')
const res = await myPromise;
console.log('执行完第一个await,继续往下执行')
console.log(await res);
console.log('执行完第二个await,继续往下执行')
setTimeout(() => console.log("Timeout!"), 0);
console.log("Last line!");
}
func1();
func2();
答案:
Last line!
进入func2
执行第一个then Promise!
执行完第一个await,继续往下执行
执行第二个then Promise!
Promise!
执行完第二个await,继续往下执行
Last line!
Timeout!
Timeout!
解析:
- 首先看整体,声明了一个
myPromise
,两个函数func1,func2
,在最下面调用了两个函数,调用顺序是先func1
后func2
; - 然后我们进入函数
func1
:在函数func1
中,又分为三个块:微任务myPromise.then
(异步)、宏任务setTimeout
(异步)、同步任务console.log
,它们都进入调用栈,同步任务进入执行栈,异步任务的响应回调进入任务队列,执行栈上的代码被执行,输出Last line!
- 接着我们进入函数
func2
:在函数func2
中,分为七个块,照代码顺序以此为:同步、异步、同步、同步、同步、异步、同步,注意,函数func2
中出现了await
,根据上文分析,函数func2
的执行顺序是:await
之前的同步-->await
的内容-->await
之后的内容,所以执行打印-->执行第一个await
包括微任务完成-->执行打印-->执行第二await
包括微任务完成-->执行打印-->宏任务进入任务队列-->执行打印 - 好的,到此我们已经结束了第一轮,该把任务队列拿出来遛遛了,任务队列中目前排队的有
func1
的setTimeout
和func2
的setTimeout
,让它们按照队列先进先出的原则依次执行;
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!