likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 24

同步 (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)

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

  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

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

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

转载自:https://juejin.cn/post/7399986979735126031
评论
请登录