浏览器事件循环
前言
在JavaScript的执行过程中,是所有代码都按顺序执行完毕吗?
一、进程和线程
进程(process)和线程(thread)是操作系统中的两个概念。
1. 进程
- 计算机
已经运行的程序,是操作系统管理程序的一种方式。 - 启动
一个应用程序,就会默认启动一个进程(也可能是多个进程)。
2. 线程
- 操作系统能够运行
运算调度的最小单位,通常情况下它被包含在进程中。 - 每
一个进程中,都会启动至少一个线程用来执行程序中的代码,这个线程被称为主线程。
总结:进程是线程的容器。

3.操作系统的工作方式
操作系统可以让多个进程同时工作。
- 因为
CPU运算速度非常快,可以在多个进程中快速切换。 - 当
进程中的线程获取到时间片,就可以快速执行代码。 - 用户感知不到这种快速的切换。
二、浏览器JavaScript线程
JavaScript是单线程的,它的容器进程有两种:
- 浏览器
- Node
1.浏览器线程
- 目前
多数的浏览器都是多进程的,打开一个tab页面就会开启一个新的进程,为了防止一个页面卡死造成所有页面无法响应,整个浏览器需要强制退出。 - 每个进程中有很多线程,其中包括执行
JavaScript代码的线程。
2.JavaScript线程
因为JavaScript的代码在一个单独的线程中执行,所以:
- JavaScript代码在
同一时刻只能做一件事(参考JavaScript的执行过程)。 - 如果这件事非常
耗时,意味着当前的线程会被阻塞。
为了线程不阻塞,耗时的操作实际上不是JavaScript线程在执行。
- 浏览器的进程是多线程的,所以
其它线程可以完成这个耗时的操作。比如网络请求、定时器等。 JavaScript线程只需要处理对应的回调函数。
总结:在JavaScript代码执行过程中,如果遇到异步操作如setTimeout,则setTimeout被放到调用栈中并立即执行结束出栈,不会阻塞后续代码的执行。
三、事件循环
在JavaScript线程中,先执行执行上下文栈中的执行上下文。当执行上下文栈为空时,开始检查浏览器是否把一些任务(DOM监听、XMLHttpRequest、定时器)加入事件队列(任务队列)里。如果事件队列不为空,则从事件队列中取出最先放进去的任务加入到执行上下文栈中执行,如此往复,将事件队列内的任务执行完毕。形成的回环就是事件循环。

四、事件队列
事件队列包含宏任务队列(macrotask queue)和微任务队列(microtask queue)。
1.宏任务队列
宏任务:ajax、setTimeout、setInterval、DOM监听、UI Rendering等。
2.微任务队列
微任务:Promise的then回调、Mutation Observer API、queueMicrotask()等。
注:Promise必须等到为fulfilled状态,即executor函数执行到resolve()之后才会把回调加入到微任务队列。
3.执行机制
main script中的代码优先执行(编写的顶层script代码)。- 在执行任何一个
宏任务之前(不是队列,是一个宏任务),先查看微任务队列中是否有任务需要执行。
宏任务执行前,必须保证微任务队列是空的。- 如果不为空,优先执行
微任务队列中的任务(回调)。
五、面试题
1.面试题1-Promise
console.log('script start')
setTimeout(function () {
console.log('setTimeout1')
new Promise((resolve, reject) => {
resolve()
}).then(res => {
console.log('then4')
})
console.log('then2')
})
new Promise((resolve, reject) => {
console.log('promise1')
resolve()
}).then(res => {
console.log('then1');
})
setTimeout(() => {
console.log('setTimeout2');
})
console.log(2);
queueMicrotask(() => {
console.log('queueMicrotask');
})
new Promise((resolve, reject) => {
resolve()
}).then(res => {
console.log('then3');
})
console.log('script end');
// script start
// promise1
// 2
// script end
// then1
// queueMicrotask
// then3
// setTimeout1
// then2
// then4
// setTimeout2
思路:
- 依次执行
main script代码,遇到微任务和宏任务时将任务加入到各自的任务队列。 - main script代码执行完毕,依次执行
微任务队列中的微任务。执行微任务时,遇到微任务和宏任务时将任务加入到各自的任务队列。 - 微任务队列执行完毕,执行
第一个宏任务。 - 执行宏任务时,遇到微任务和宏任务时将任务加入到各自的任务队列。
- 执行完一个宏任务,检查并执行
微任务队列中的微任务。 - 如此往复。执行完微任务队列和宏任务队列中的任务。
2.面试题2-接口请求
2.1 promise.then
console.log('script start');
function requestData (url) {
return new Promise((resolve) => {
setTimeout(() => {
console.log('setTimeout');
resolve(url)
}, 2000)
})
}
function getData () {
console.log('getData start');
requestData('why').then(res => {
console.log('then1-res:', res);
})
console.log('getData end');
}
getData()
console.log('script start');
// script start
// getData start
// getData end
// script end
// setTimeout
// then1-res:why
思路:
- 注意Promise必须等到为fulfilled状态,即executor函数执行到resolve()之后才会把回调加入到微任务队列。
2.2 async/await
console.log('script start');
function requestData (url) {
return new Promise((resolve) => {
setTimeout(() => {
console.log('setTimeout');
resolve(url)
}, 2000)
})
}
async function getData () {
console.log('getData start');
const res = await requestData('why')
console.log('then1-res:', res);
console.log('getData end');
}
getData()
console.log('script start');
// script start
// getData start
// script start'
// setTimeout
// then1-res:why
// getData end
思路:
- await之后的代码必须等到Promise的状态变成fulfilled才会执行。
3.面试题3
async function async1 () {
console.log('async1 start');
await async2()
console.log('async1 end');
}
async function async2 () {
console.log('async2');
}
console.log('script start');
setTimeout(() => {
console.log('timeout');
}, 0);
async1()
new Promise(resolve => {
console.log('promise1');
resolve()
}).then(res => {
console.log('promise2');
})
console.log('script end');
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// timeout
思路:
- 执行
async2()时,相当于return undefined => Promise.resolve(undefined),所以console.log('async1 end')会被加入到微任务队列中。
附录
- 视频:浏览器事件循环
转载自:https://juejin.cn/post/7235967072528957499