浏览器事件循环
前言
在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