浏览器事件循环

发布于 2023-11-30 16:54:03 字数 3476 浏览 35 评论 0

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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

拍不死你

暂无简介

文章
评论
603 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文