浏览器是多线程的(否则一些异步的事件谁来处理?),浏览器的主要线程有以下几种:
- js 引擎线程
- 事件触发线程
- 定时器触发线程
- 异步 http 线程
- GUI 渲染线程
因为 js 引擎是单线程的,所以当遇到定时器、DOM 事件监听的时候,js 引擎会交给相应的线程,等处理完毕后再把这些事件对应的回调放到 js 引擎中的任务队列中。这个任务队列就是和 Event Loop 息息相关的。
微任务和宏任务
上述的任务队列一共有两种,微任务和宏任务。
宏任务macrotask
:
setTimeout
setInterval
xhr
的回调dom
事件监听的回调
微任务microtask
:
Promise
MutationObserver
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
。