🔥EventLoop是什么?执行机制是什么?


image.png

这是我参与更文挑战的第20天,活动详情查看: 更文挑战javascript

前言

面试官:给我说说事件循环吧,就是EventLoop的机制,这个你据说过吧。html

我:...💥💥💥🚀java

什么是 EventLoop ?

先看这张图,先无论宏任务,微任务是什么,先看整个流程。web

image-20210406150156448

分析:面试

  1. 判断宏任务队列是否为空promise

    • 不空 --> 执行最先进入队列的任务 --> 执行下一步
    • 空 --> 执行下一步
  2. 判断微任务队列是否为空浏览器

    • 不空 --> 执行最先进入队列的任务 --> 继续检查微任务队列空不空
    • 空 --> 执行下一步

由于首次执行宏队列中会有 script(总体代码块)任务,因此实际上就是 Js 解析完成后,在异步任务中,会先执行完全部的微任务,这里也是不少面试题喜欢考察的。须要注意的是,新建立的微任务会当即进入微任务队列排队执行,不须要等待下一次轮回。markdown

主线程从任务队列读取事件,这个过程是循环不断的,因此整个运行机制又称为 Event Loop(事件循环)。数据结构

在深刻事件循环机制以前,须要弄懂一下几个概念:异步

  • 执行上下文( Execution context )
  • 执行栈( Execution stack )
  • 微任务( micro-task )
  • 宏任务( macro-task )

执行上下文( Execution context )

执行上下文是一个抽象的概念,能够理解为是代码执行的一个环境。JS 的执行上下文分为三种,全局执 行上下文、函数(局部)执行上下文、Eval 执行上下文。

  • 全局执行上下文:全局执行上下文指的是全局 this 指向的 window ,能够是外部加载的 JS 文件 或者本地 标签中的代码。
  • 函数执行上下文:函数上下文也称为局部上下文,每一个函数被调用的时候,都会建立一个新的局部 上下文。
  • Eval 执行上下文: 这个不经常使用,这里不说。

执行栈( Execution stack )

执行栈,就是咱们数据结构中的“栈”,它具备“先进后出”的特色,正是由于这种特色,在咱们代码进行 执行的时候,遇到一个执行上下文就将其依次压入执行栈中。

当代码执行的时候,先执行位于栈顶的执行上下文中的代码,当栈顶的执行上下文代码执行完毕就会出 栈,继续执行下一个位于栈顶的执行上下文。

function foo() {
    console.log('a');
    bar();
    console.log('b');
}
function bar() {
    console.log('c');
}
foo();
复制代码
  1. 初始化状态,执行栈任务为空。
  2. foo 函数执行, foo 进入执行栈,输出 a ,碰到函数 bar 。
  3. 而后 bar 再进入执行栈,开始执行 bar 函数,输出 c 。
  4. bar 函数执行完出栈,继续执行执行栈顶端的函数 foo ,最后输出 b 。
  5. foo 出栈,全部执行栈内任务执行完毕。

什么是宏任务与微任务?

咱们都知道 Js 是单线程都,可是一些高耗时操做就带来了进程阻塞问题。为了解决这个问题,Js 有两种任务的执行模式:同步模式(Synchronous)和异步模式(Asynchronous)

在异步模式下,建立异步任务主要分为宏任务与微任务两种。ES6 规范中,

  • 宏任务(Macrotask) 称为 Task, 微任务(Microtask) 称为 Jobs。
  • 宏任务是由宿主(浏览器、Node)发起的,而微任务由 JS 自身发起。
宏任务与微任务的几种建立方式 👇
宏任务(Macrotask) 微任务(Microtask)
setTimeout requestAnimationFrame(有争议)
setInterval MutationObserver(浏览器环境)
MessageChannel Promise.[ then/catch/finally ]
I/O,事件队列 process.nextTick(Node环境)
setImmediate(Node环境) queueMicrotask
script(总体代码块)

注意:nextTick 队列会比 Promie 队列先执行。

如何理解 script(总体代码块)是个宏任务呢 🤔

实际上若是同时存在两个 script 代码块,会首先在执行第一个 script 代码块中的同步代码,若是这个过程当中建立了微任务并进入了微任务队列,第一个 script 同步代码执行完以后,会首先去清空微任务队列,再去开启第二个 script 代码块的执行。

因此这里应该就能够理解 script(总体代码块)为何会是宏任务。

以上概念弄明白以后,再来看循环机制是如何运行的呢?如下涉及到的任务执行顺序都是靠函数调用栈 来实现的。

  1. 首先,事件循环机制的是从script标签内的代码开始的,上边咱们提到过,整个script标签 做为一个宏任务处理的。
  2. 在代码执行的过程当中,若是遇到宏任务,如:setTimeout,就会将当前任务分发到对应的执行队 列中去。
  3. 当执行过程当中,若是遇到微任务,如: Promise ,在建立 Promise 实例对象时,代码顺序执行,若是 到了执行· then 操做,该任务就会被分发到微任务队列中去。
  4. script 标签内的代码执行完毕,同时执行过程当中所涉及到的宏任务也和微任务也分配到相应的队 列中去。
  5. 此时宏任务执行完毕,而后去微任务队列执行全部的存在的微任务。
  6. 微任务执行完毕,第一轮的消息循环执行完毕,页面进行一次渲染。
  7. 而后开始第二轮的消息循环,从宏任务队列中取出任务执行。
  8. 若是两个任务队列没有任务可执行了,此时全部任务执行完毕。

定时器 setTimeout

任务队列除此以外,还能够放定时器的回调函数,须要指定某些代码多少时间以后执行。

定时器主要包括两种, setTimeout 和 setInterval 两个函数。 当咱们设置定时器的时间,执行某个特定的任务,以下:

// 1 秒后执行
  setTimeout(function () {
    console.log(2);
  }, 1000);
  console.log(1)


复制代码

上述的输出结果为 1, 2,执行完同步代码后,就会执行定时器中的任务事件

// 同步执行完当即执行
  setTimeout(function () {
    console.log(2);
  }, 0);
  console.log(1)
复制代码

当咱们执行 setTimeout(fn,0) 定时器时,会将这个定时任务回调放在任务队列的尾部,表明的含 义就是尽早的执行。

也就是等到主线程同步任务和"任务队列"现有的事件都处理完,而后才会当即执行这个定时器的任务。

上述的前提是,等到同步任务和任务队列的代码执行完毕后,若是当前代码执行很长时间,定时器并没 办法保证必定在指定时间执行。

注:HTML5 标准规定了setTimeout() 的第二个参数的最小值(最短间隔),不得低于4毫秒, 若是低于这个值,就会自动增长。

若是涉及到页面的改动,这个定时器任务一般不会当即执行,而是 16 毫秒执行一次,咱们一般使用 requestAnimationFrame() 。

小实战

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>消息运行机制</title>
</head>

<body>
  
</body>
<script> console.log('1'); setTimeout(() => { console.log('2') }, 1000); new Promise((resolve, reject) => { console.log('3'); resolve(); console.log('4'); }).then(() => { console.log('5'); }); console.log('6');// 1,3,4,6,5,2 </script>

</html>
复制代码

分析:

  • 初始化状态,执行栈为空。
  • 首先执行<script>标签内的同步代码,此时全局的代码进入执行栈中,同步顺序执行代码,输出 1。
  • 执行过程当中遇到异步代码 setTimeout (宏任务),将其分配到宏任务异步队列中。
  • 同步代码继续执行,遇到一个 promise 异步代码(微任务)。可是构造函数中的代码为同步代码,依次输出三、4,则then以后的任务加入到微任务队列中去。
  • 最后执行同步代码,输出 6。
  • 由于 script 内的代码做为宏任务处理,因此这次循环进行处处理微任务队列中的全部异步任务,直达微任务队列中的全部任务执行完成为止,微任务队列中只有一个微任务,因此输出 5。
  • 此时页面要进行一次页面渲染,渲染完成以后,进行下一次循环。
  • 在宏任务队列中取出一个宏任务,也就是以前的 setTimeout,最后输出 2。
  • 此时任务队列为空,执行栈中为空,整个程序执行完毕。

image-20210406152314952

以上不免有些啰嗦,因此简化整理以下步骤:

  • 一开始执行宏任务( script 中同步代码),执行完毕,调用栈为空。
  • 而后检查微任务队列是否有可执行任务,执行完全部微任务。
  • 进行页面渲染。
  • 第二轮从宏任务队列取出一个宏任务执行,重复以上循环。
相关文章
相关标签/搜索