一、浏览器JS异步执行的原理
一般常说js是一门单线程语言,那为什么可以异步执行且不发生阻塞呢?
- 常说的JS是单线程语言,是因为执行JS的引擎是单线程的,而浏览器本身是多线程的
- 浏览器主要含有:
- js 执行线程
- 定时器线程
- http 请求线程
- 事件触发线程
- GUI 线程等
- 异步请求的真正执行者是浏览器的其他线程
- js 引擎只是执行了异步操作成功了之后的回调函数
二、事件循环机制
1 - 执行栈和任务队列
(1)执行栈是什么
- 用于按执行顺序存放同步代码
- 按序执行,执行完毕后弹出执行栈
- 如果在执行过程中遇到异步操作,就交给其他线程处理
(2)任务队列
- 用于按序排放异步操作执行结束后的回调函数
- 任务队列中的函数等待执行栈执行结束后取出执行
2 - 事件循环的本质
- 基于事件驱动模式
- 至少包含了一个事件循环来判断当前的任务队列是否有新的任务
- 通过不断的循环取出异步回调进行执行
3 - 宏任务和微任务
(1)宏任务的分类
- 渲染事件
- 用户交互事件
- SetTimeout、setInterval
- 网络请求、文件读写等
有明确异步操作的任务,需要其他的异步线程支持
(2)微任务的分类
- promise.then promise.catch
- process.nextTick
没有明确的异步任务需要执行,只有回调不需要其他异步线程的支持
(3)执行顺序
- 在同步代码执行结束后
- 先执行微任务队列中的微任务
- 再执行宏任务队列中的宏任务
(4)案例
console.log('同步代码1');
setTimeout(() => {
console.log('setTimeout')
}, 0)
new Promise((resolve) => {
console.log('同步代码2')
resolve()
}).then(() => {
console.log('promise.then')
})
console.log('同步代码3');
// 最终输出"同步代码1"、"同步代码2"、"同步代码3"、"promise.then"、"setTimeout"
setTimeout(() => {
console.log('setTimeout start');
new Promise((resolve) => {
console.log('promise1 start');
resolve();
}).then(() => {
console.log('promise1 end');
})
console.log('setTimeout end');
}, 0);
function promise1() {
return new Promise((resolve) => {
console.log('promise2');
resolve();
})
}
async function async1() {
console.log('async1 start');
await promise1();
console.log('async1 end');
}
async1();
console.log('script end');
// 输出结果
async1 start
promise2
script end
async1 end
setTimeout start
promise1 start
setTimeout end
promise1 end
4 - 定时器误差
(1)执行顺序
- 遇到一个定时器请求,开启定时器线程去计时
- 计时结束后将回调函数放入到任务队列(宏任务)
- 等待同步任务执行 waiting…… ,在同步任务执行结束后可能还有微任务
- 同步任务执行结束后取出任务队列中的回调
(2)误差大小
- 定时器的误差值取决于同步任务的执行时间 + 微任务的执行时间
- 同步任务执行时间越长,定时器的误差越大