Event Loop
JS是一门单线程的非阻塞的脚本语言,只有一个主线程来处理所有任务。当JS执行一系列任务时,由于JS是单线程的,同一时间只能处理一个任务,于是这些任务就在执行栈中排队,JS会依次执行这些任务。当JS执行一项异步任务(如I/O)时,主线程不会一直等待其返回结果,而是挂起(pending)这个任务,继续执行执行栈中的其他任务,等异步任务返回结果时再根据一定规则去执行相应的回调。当异步任务返回结果时,JS会将这个异步任务加入与当前执行栈不同的另一个队列--事件队列。被加入事件队列的异步任务的回调不会立即被执行,而是等待当前执行栈中的任务执行完毕,主线程闲置时,JS从事件队列中,取出排在第一位的任务,将对应回调放入执行栈,并执行其中的同步代码,如此反复,形成一个无限循环,称为事件循环(Event Loop)。
Macro Task 和 Micro Task
异步任务并不相同,它们的执行顺序也有差异。不同的异步任务分为两类:宏任务(Macro Task)和微任务(Micro Task)。
以下事件属于宏任务:
- setTimeout()
- setInterval()
以下事件属于微任务:
- Promise.then()
当前执行栈中任务执行完毕时,JS会查看微任务队列中是否有事件,如果没有,会去宏任务队列中取出排在第一位的事件并把其对应回调放入当前执行栈执行;如果微任务队列中有事件,会依次执行事件对应的回调,直到微任务队列清空,再去宏任务队列中取出排在第一位的事件并把其对应回调放入当前执行栈执行,如此循环。即同步代码执行完毕之后,先清空微任务,再执行第一个宏任务,同一次事件循环中,微任务永远在宏任务之前执行。
这样就能解释以下代码:
console.log('script start')
let timer1 = setTimeout(() => {
console.log('timer1')
let promise1 = new Promise((resolve, reject) => {
console.log('timer1-Promise1')
resolve()
})
promise1.then(() => {
console.log('timer1-Promise1-then')
})
}, 0)
let timer2 = setTimeout(() => {
console.log('timer2')
}, 0)
let promise2 = new Promise((resolve, reject) => {
console.log('Promise2')
resolve()
})
promise2
.then(() => {
console.log('Promise2-then1')
})
.then(() => {
console.log('Promise2-then2')
})
console.log('script end')
执行结果:
简单分析:
主线程依次执行这段代码,同步代码直接执行,异步代码挂起,加入事件队列。
以下代码直接执行:
console.log('script start')
let promise2 = new Promise((resolve, reject) => {
console.log('Promise2')
resolve()
})
console.log('script end')
注意:作为Promise参数的这个函数是同步代码,会直接执行。因此前三个打印结果依次是
script start
Promise2
script end
以下事件被加入宏任务:
timer1
timer2
以下事件被加入微任务:
promise2
.then()
.then()
同步代码执行完毕,执行栈清空,主线程查看微任务队列,执行相应回调清空微任务队列,打印出
Promise2-then1
Promise2-then2
微任务队列清空完毕后,主线程查看宏任务队列,执行排在第一位的事件(timer1)对应回调。
timer1的回调又开始了新的Event Loop:
同步代码:
console.log('timer1')
let promise1 = new Promise((resolve, reject) => {
console.log('timer1-Promise1')
resolve()
})
直接执行,打印出:
timer1
timer1-Promise1
以下事件加入微任务:
promise1.then()
同步代码执行完毕,清空微任务队列,打印出:
timer1-Promise1-then
此时宏任务队列排在第一位的是timer2,主线程取出timer2执行相应回调,打印出:
timer2
整个脚本执行完毕。