浏览器是多线程的(否则一些异步的事件谁来处理?),浏览器的主要线程有以下几种:
- js 引擎线程
- 事件触发线程
- 定时器触发线程
- 异步 http 线程
- GUI 渲染线程
因为 js 引擎是单线程的,所以当遇到定时器、DOM 事件监听的时候,js 引擎会交给相应的线程,等处理完毕后再把这些事件对应的回调放到 js 引擎中的任务队列中。这个任务队列就是和 Event Loop 息息相关的。
微任务和宏任务
上述的任务队列一共有两种,微任务和宏任务。
宏任务macrotask:
setTimeoutsetIntervalxhr的回调dom事件监听的回调
微任务microtask:
PromiseMutationObserver
Event Loop
Event Loop是一个执行机制,在浏览器和nodejs分别有不同的实现,本文均是以浏览器的Event Loop为例。
当 js 引擎执行全局script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(执行完后会分别放入相应的任务队列);
- 全局
script代码执行完毕后,调用栈call stack会清空; - 从微队列
microtask queue中取出位于队首的回调任务,放入调用栈call stack中执行,执行完后microtask queue长度减 1,这样重复直到清空队列注意,如果在执行 microtask 的过程中,又产生了 microtask,那么会加入到队列的末尾,也会在这个周期被调用执行; - 取出宏队列
macrotask queue中位于队首的任务,放入call stack中执行; - 执行完毕重复操作
2、3、4,直至微队列和宏队列全部清空。
来个例子
看下面一个例子的输出(网上随便找的,还蛮有典型性的):
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
// 在谷歌浏览器上最后的输出:
script start
async2 end
Promise
script end
async1 end
promise1
promise2
setTimeout
重要的几点:
Promise的入参是一个立即执行的方法,所以Promise会在第一次循环时输出;Promise的then方法入参如果没有返回一个promise对象,会默认返回一个fulfilled状态的promise,所以两个 then 里的回调都会放到同一个微任务队列中,最后同时输出;async、await都会转为promise。