搞懂浏览器中的事件循环 (Event Loop)

同步 (synchronous) 与 异步 (asynchronous)

在讨论事件循环前,我们需要先了解同步与异步的概念。JavaScript 是单线程的程序语言,一行代码执行完才会再执行下一行,这个概念称之为 **同步 (synchronous)**。但这样其实会遇到一个问题,试想一个场景:假设有一网站需要去服务器端获取数据,但需要等十秒之后才会拿到,此外等待途中网站无法执行任何操作,这对于使用者来说,会认为页面卡住十秒钟、就像宕机一样,这绝对是很糟糕的体验,于是就有了 异步 (asynchronous) 的概念。

异步的代码或事件,并不会阻碍主线程执行其他代码,以上面网站向服务器获取数据为例,获取数据当作是一个异步事件,异步事件会在完成之后再通知主线程,而在这之中,主线程可以继续执行其他代码、异步事件也不会阻塞用户操作。而浏览器或其他的执行环境 (例如 Node.js) 之所以能够实现 异步,正是因为有 事件循环 (Event loop) 的机制。通过事件循环机制,能有效解决 JavaScript 单线程的问题,让耗时的操作不会阻塞主线程。

事件循环 (Event loop) 的组成

事件循环不存在 JavaScript 本身,而是由 JavaScript 的执行环境 (浏览器或 Node.js) 来实现的,其中包含几个概念:

  • 堆 (Heap):堆是一种数据结构,拿来储存对象
  • 栈 (Stack):采用后进先出的规则,当函数执行时,会被添加到栈的顶部,当执行完成时,就会从顶部移出,直到栈被清空
  • 队列 (Queue):也是一种数据结构,特性是先进先出 (FIFO)。在 JavaScript 的执行环境中,等待处理的任务会被放在队列 (Queue) 里面,等待栈 (Stack) 被清空时,会从队列 (Queue) 中获取第一个任务进行处理
  • 事件循环 (Event loop):事件循环会不断地去查看栈 (Stack) 是否空出,如果空出就会把队列 (Queue) 中等待的任务放进栈 (Stack) 中执行
搞懂浏览器中的事件循环 (Event Loop)
Event Loop 的堆 (Heap)、栈 (Stack) 和队列 (Queue)

事件循环 (Event loop)

整个事件循环大概可以分为几个步骤

  1. 所有任务都会在主线程上执行,形成一个执行栈
  2. 如果遇到异步任务,例如:setTimeout,执行环境会调用相关的 API (例如在浏览器上会调用 Web API),等待此异步任务的结果之后,再被放置到任务队列中
  3. 一旦执行栈的所有同步任务完成之后,就会读取任务队列,并将任务队列第一个,加到执行栈中运行
  4. 只要执行栈空了之后,就会读取任务队列,不断重复这个步骤,直到所有任务完成,这个流程就是**事件循环 (Event loop) **

宏任务 (Macro Task) 与微任务 (Micro Task)

除了事件循环的流程以外,面对这个面试题,宏任务 (Macro Task) 与微任务 (Micro Task) 也是必提的概念。JavaScript 中的异步任务又分成宏任务 (Macro Task) 和微任务 (Micro Task),这两者的执行顺序是不同的。如果不分清楚这两种类型的任务,很可能程序执行出的顺序会跟预期的不同。

举例来说,下面这段代码,打打印出的顺序会是什么呢?

console.log(1);

setTimeout(function ({
  console.log(2);
}, 0);

Promise.resolve()
  .then(function ({
    console.log(3);
  })
  .then(function ({
    console.log(4);
  });

假如只单纯区分同步与异步,可能会回答 1234;但是正确答案应该是 1342。为什么是 1342? setTimeout 不是设置 0 毫秒,这样为什么会是 Promise 里面的东西先执行呢?原因是 Promise 会进到微任务队列,而 setTimeout 会是在宏任务队列。在一次事件循环中,宏任务一次只提取一个,所以 console.log(1) 后,会先去看微任务队列,不断提取到执行栈中直到微任务队列为空,因此这边会先执行 Promise ,然后才是 setTimeout

常见的宏任务与微任务如下:

  • 宏任务:script(整体代码)、 setTimeoutsetInterval、I/O、事件、 postMessageMessageChannelsetImmediate (Node.js)
  • 微任务:Promise.thenMutaionObserverprocess.nextTick (Node.js)。

执行顺序如下:

  • 执行一次宏任务 (最开始会是整个 srcipt 所以上面的例子会先执行 console.log(1)
  • 执行过程中如果遇到宏任务,就放进宏任务队列
  • 执行过程中如果遇到微任务,就放进微任务队列
  • 当执行栈空了,先检查微任务队列,如果有微任务,就依序执行直到微任务队列为空
  • 接着进行浏览器的渲染,渲然完后开始下一个宏任务 (回到最开始的步骤)

延伸

在事件循环的面试题中,也会问到 requestAnimationFramerequestIdleCallback 在事件循环中的发生时机点。requestAnimationFrame 发生的顺序会是在下次页面重绘之前操作 (style calculation、layout、paint 这些渲染步骤前),因为浏览器在每次事件循环中,不一定会重新绘制页面;因此 requestAnimationFrame 执行时机点其触发时间点跟任务队列关系比较小,而是跟页面重绘关系比较大。

requestIdleCallback 则是在浏览器渲染后,如果有空闲时间时则会触发。


原文始发于微信公众号(程序猿技术充电站):搞懂浏览器中的事件循环 (Event Loop)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/298513.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!