浏览器事件循环
JS 是单线程的,但浏览器却不是,浏览器是多线程的结构。里面包含 UI 渲染线程、JS 线程、网络线程等等。而对 JS 来说,因为是单线程,这意味着同一时间只能执行一个任务,其它的任务要被阻塞排队。当 JS 线程在执行的时候,UI 渲染线程是被阻塞住的,这很好理解,因为 JS 可以修改 DOM,所以必须确保 JS 执行完后才来进行更新渲染。
那么这样带来的问题是,如果一个任务执行时间过长,不仅其它任务在后面排队等待,而且会导致页面无法响应用户其它点击事件,从而造成页面卡死的状态。比如经常在浏览器里会见到一个提示说「当前页面无响应」然后让你选择重新加载还是关闭掉。为了解决这个问题,浏览器需要有一个机制,来充分利用各种资源调度执行好这些 JS 任务,这就是事件循环。
事件循环的过程
来看一张经典图:
上面这张图里有三个信息:
- JS 内存堆和调用栈,调用栈是先进后出的结构
- 宿主环境(浏览器)提供的 API:setTimeout、DOM、AJAX ...
- 事件循环回调队列
在 JS 里,任务分为两种:
- 同步任务:在主线程上执行,只有前面的任务执行完,后面的才能接着执行
- 异步任务:执行后先挂起,然后等待回调进入任务队列,任务队列再通知主线程来执行
我们平时写的每一个 JS 程序,JS 引擎都会分析,按照代码块逐步执行,比如下面这段代码:
console.log('hello')
setTimeout(function() {
console.log('timeout')
}, 0)
console.log('world')
它的执行过程如下:
- JS 调用栈初始为空,遇到
console.log('hello')
,将它推入调用栈 - 执行
console.log('hello')
,控制台打印hello
,执行完毕,将它出栈 - 接着碰到
setTimeout(function() {}, 0)
,将它推入调用栈 - 开始执行 setTimeout ,发现它是个定时器,于是往 Web API 里添加 timer 计时任务
- setTimeout 执行完毕,出栈
- 接着碰到
console.log('world')
,将它推入调用栈 - 执行
console.log('world')
,控制台打印world
。执行完毕,出栈 - 这个时候调用栈为空了,前面不是有个 setTimeout 在计时吗,当它计时完毕后,就会往宏任务队列里添加一个 cb 回调
- 事件循环发现宏任务队列里还有个 cb 回调,于是将它取出,推入调用栈
- 开始执行 cb 回调代码,发现里面有一句
console.log('timeout')
,于是将它推入调用栈 - 开始执行
console.log('timeout')
,控制台打印timeout
。执行完毕,这句语句出栈 - cb 执行完毕,出栈
以上就是事件循环的一个简单过程。
宏任务和微任务
事件循环的过程中,根据任务的特点会将任务放入两个队列,分别是 宏任务
和 微任务
。
宏任务代码主要有:
- setTimeout
- 用户交互事件(鼠标点击,滚动页面)
- script 块代码
微任务主要有:
- promise
- MutationObserver
为什么会出现微任务?与宏任务的关系是什么?
答:一个任务如果是同步执行会影响效率,如果是异步执行,又影响实时性。微任务就是在效率和实时性取得一个平衡。我们把消息队列中的任务称为宏任务,而每个宏任务又有自己的微任务队列,用来存放执行过程中产生的新任务。
主线程最开始会先取出一个宏任务执行,在这个过程中不断往调用栈添加新的代码执行,当执行完一个宏任务后,引擎会去查看微任务队列,看是否有任务需要执行,没有就继续取出宏任务执行,开启下一个事件循环。
事件循环的应用
前面说到,如果一个任务执行时间过长,后面的任务就必须挂起等待。现在有了这个事件循环机制后,我们就可以针对性地对代码做一些优化。具体的优化方式就是通过 setTimeout
来 将大的任务拆分为多个小任务
来避免一个任务耗时过长。
一道经典的执行顺序考察题
function promise1() {
return new Promise((resolve) => {
console.log('promise1 start');
resolve();
})
}
function promise2() {
return new Promise((resolve) => {
console.log('promise2 start');
resolve();
})
}
function promise3() {
return new Promise((resolve) => {
console.log('promise3 start');
resolve();
})
}
function promise4() {
return new Promise((resolve) => {
console.log('promise4 start');
resolve();
}).then(() => {
console.log('promise4 end');
})
}
async function asyncFun() {
console.log('async1 start');
await promise2();
console.log('async1 inner');
await promise3();
console.log('async1 end');
}
setTimeout(() => {
console.log('setTimeout start');
promise1();
console.log('setTimeout end');
}, 0);
asyncFun();
promise4();
console.log('script end');
上面这段代码在控制台里会输出什么?可以自己试着分析下。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论