事件循环
前言
事件循环机制又叫消息循环机制(EventLoop/MessageLoop),主要描述了JavaScript
同步任务和异步任务的执行规则。
基础知识储备:
- 进程和线程大概了解是什么
- 同步和异步大概了解是什么
- 队列这种数据结构了解
为什么需要事件循环?
首先声明这个问题暂时不需要知道什么是事件循环。我们就按照代码是顺序执行的前提往下推(JavaScript是单线程的)。我们知道JavaScript
代码会被拆分成很多任务,任务执行任务过程中也会生成任务。如果所有的代码都可以无阻塞的顺利执行成功那么就没有什么问题。但如果执行过程中会去访问网络资源或者延时执行,都需要花费时间才能执行的任务,那么代码的执行就会在此处停止,导致页面无法操作造成卡顿,会严重影响用户体验。为了解决这个问题,就产生了事件循环机制来解决这个问题。
JavaScript
是单线程的,由于用户操作是有顺序的,就必须保证操作的顺序的完整性,不能操作A以后立马操作B拿到的还是操作A以前的东西,所以JavaScript
就是用了单线程来解决这个问题。
同步执行(没有事件循环)的示意图:
事件循环机制是什么?
上述代码的执行属于单一任务队列,事件循环机制主要就是利用了多任务队列,早期包含微任务队列(microQueue)和宏任务队列(macroQueue)后来宏任务队列不能满足复杂的业务场景W3C也不再限制,由各个浏览器厂商自行实现,V8就把宏任务队列拆分成了很多任务队列,比如延时队列和交互队列等。这里就是用宏任务队列和微任务队列进行讲解。
微任务:浏览器环境内微任务主要包括:Promise生成的任务,MutationObserver成成的任务以及queueMicroTask生成的任务
宏任务:除了同步任务和微任务其余统称宏任务,比如setTimeOut, setTimeInterval和用户事件
注:浏览器规定在所有宏任务中用户操作要排在最前面,因为用户操作被认为是最紧急的。
那么当代码执行的时候只会将代码推入任务主线程进行执行,期间产生的异步任务会根据产生的方法自动分配到微任务和宏任务队列,当所有的同步代码执行完毕后,会去执行为任务队列的任务,微任务队列中的任务执行完毕后,会去执行宏任务队列内的任务,最后宏任务队列执行完毕,跳出事件循环。
步骤如下:
- 将同步代码推入主线程,执行同步代码,同步代码产生异步任务,将产生的异步任务推入宏任务或微任务队列,继续执行同步代码,直到没有同步代码可以执行。
- 检查是否有同步任务需要执行,若有执行同步任务。若无将微任务队列的头部任务推出,推入到主线程执行任务,任务可能产生异步任务,将产生的异步任务推入宏任务或微任务队列,继续执行步骤2,直到没有微任务可以执行。
- 检查是否有同步任务需要执行,若有执行同步任务。若无,检查是否存在微任务需要执行,若有执行步骤2。若无,将宏任务队列的头部任务推出,推入到主线程执行任务,任务可能产生异步任务,将产生的异步任务推入宏任务或微任务队列,继续执行步骤3,直到没有宏任务可以执行。
这就是事件循环的一个逻辑。
样例代码:
function sync1() {
var taskid = 1
console.log(taskid)
}
function sync2() {
var taskid = 2
console.log(taskid)
}
function sync3() {
var taskid = 3
var mi1 = new Promise((resolve) => {
var taskid = "mi1"
resolve(taskid)
})
mi1.then(id => {
console.log(id)
var mi2 = new Promise(resolve => {
var taskid = "mi2"
resolve(taskid)
})
mi2.then((id) => {
console.log(id)
const ma1 = setTimeout(() => {
var taskid = "ma1"
console.log(taskid)
}, 0)
})
})
console.log(taskid)
}
function sync4() {
var taskid = 4
console.log(taskid)
}
function sync5() {
var taskid = 5
const ma2 = setTimeout(() => {
var taskid = "ma2"
var mi3 = new Promise((resolve) => {
const taskid = "mi3"
resolve(taskid)
})
mi3.then((id) => {
console.log(id)
})
var ma3 = setTimeout(() => {
var taskid = "ma3"
console.log(taskid)
},0)
console.log(taskid)
},0)
console.log(taskid)
}
function sync6() {
var taskid = 6
console.log(taskid)
}
sync1()
sync2()
sync3()
sync4()
sync5()
sync6()
事件循环机制下代码执行示意图:
- 任务初始化
- 主线程执行同步代码
顺利执行任务1和2,到任务3时产生mi1微任务,将mi1放入微任务队列
输出:1,2,3
- 主线程继续执行同步代码
顺利执行任务4,到任务5时产生ma2宏任务,将ma2放入宏任务队列
输出:1,2,3,4,5
- 主线程继续执行同步代码
任务6执行完成无同步任务可以执行,开始上述步骤2
输出:1,2,3,4,6
- 按照上述步骤2执行
检查是否有同步任务需要执行==>无。取出microQueue第一个任务mi1放入主线程执行,产生微任务mi2,放入微任务队列。
输出:1,2,3,4,5,6,mi1
- 按照上述步骤2执行
检查是否有同步任务需要执行==>无。取出microQueue第一个任务mi2放入主线程执行,产生宏任务ma1,放入宏任务队列。
输出:1,2,3,4,5,6,mi1,mi2
- 按照步骤3执行
检查是否有同步任务需要执行==>无。检查是否有微任务==>无。取出macroQueue第一个任务ma2放入主线程执行,产生宏任务ma3和微任务mi3,分别放入宏任务队列和微任务队列。
输出:1,2,3,4,5,6,mi1,mi2,ma2
- 按照步骤3执行
检查是否有同步任务需要执行==>无。检查是否有微任务==>有,进入步骤2。
检查是否有同步任务需要执行==>无。执行mi3,无微任务可执行,继续执行步骤三。
输出:1,2,3,4,5,6,mi1,mi2,ma2,mi3
- 继续执行步骤3
检查是否有同步任务需要执行==>无。检查是否有微任务==>无。取出macroQueue第一个任务ma1放入主线程执行。
输出:1,2,3,4,5,6,mi1,mi2,ma2,mi3,ma1
- 继续执行步骤3
检查是否有同步任务需要执行==>无。检查是否有微任务==>无。取出macroQueue第一个任务ma3放入主线程执行。
输出:1,2,3,4,5,6,mi1,mi2,ma2,mi3,ma1,ma3
- 继续执行步骤3
检查是否有同步任务需要执行==>无。检查是否有微任务==>无。无宏任务可执行进入休眠状态。
最终输出:1,2,3,4,5,6,mi1,mi2,ma2,mi3,ma1,ma3