likes
comments
collection
share

JavaScript事件循环机制:深入解析Event loop

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

前言

JavaScript 是一种单线程的编程语言,这意味着它在任何给定的时刻只能执行一个任务。然而,JavaScript 却能够处理异步操作,实现非阻塞的编程。这一切得益于 JavaScript 的事件循环机制,通常被称为 Event Loop。

先看一段代码(面试题),如果你能够十分清楚的知道他们的执行顺序,相信这篇文章对你的帮助也不会很大,为了节省时间,下面的就没必要看了^.^ 。

console.log('script start') 
async function async1() {
  await async2()
  console.log('async1 end') 
}
async function async2() {
  console.log('async2 end') 
}
async1()
setTimeout(function () {
  console.log('setTimeout') 
}, 0)
new Promise(resolve => {
  console.log('Promise') 
  resolve()
})
  .then(function () {
    console.log('promise1') 
  })
  .then(function () {
    console.log('promise2') 
  })
console.log('script end') 

如果上面代码你都能搞懂,相信你也已经深刻理解Event loop了。

什么是Event loop?

为了揭开上述代码的面纱,我们首先得先了解一下 Event Loop 的基本概念。Event Loop 是 JavaScript 引擎的一部分,负责协调和管理代码的执行。它通过不断轮询任务队列,确保每个任务按照特定的顺序得到执行。这个循环过程包括同步代码执行、微任务执行、渲染阶段、宏任务执行等不同的阶段。

听完还是懵的? 的确,抽象的概念通常总是晦涩难懂的,学会跑步之前总是先学会走路对吧。下面我们补充一些了解Event loop之前必备的知识。

JS单线程

在前言中,我们曾说过JS是一门单线程语言,什么是线程呢?通俗来说,线程是进程中更小的单位,它描述了一段指令执行的所需的时间。而进程是CPU运行指令和保存上下文所需要的时间。在多任务操作系统中,一个进程可以包含多个线程,每个线程都是进程中独立运行的执行流。

为什么要把JS设置成单线程呢?

单线程语言,这意味着在任何给定的时刻,只能有一个任务在执行。这样相对于多线程速度不是更加缓慢吗?的确,这也正是单线程带来的最大的缺点。但是之所以以前将JS设置为单线程主要原因是为了避免带来更多的内存占用,单线程避免了多线程所需的额外资源和开销。在资源有限(JS处理的东西相比于其他语言通常不会太复杂)的环境中,反而可以提高性能。而正是因为JS为单线程,在处理用户交互和异步操作方面单线程通常会带了很大的弊端,但是结合事件循环机制(Event loop)使得JS能够处理异步任务而不阻塞主线程。下面将会介绍一下异步的一些基本概念。

异步

  • JavaScript 中的异步编程是一项重要的特性,允许代码在执行过程中处理非阻塞的任务。为了更好地理解异步编程,我们需要熟悉宏任务和微任务的概念,以及它们在事件循环中的角色。

宏任务

宏任务指的是一组按顺序执行的任务,它们在事件循环的不同阶段被调度执行。下面我将会介绍一些常见的宏任务:

  • script:整个脚本作为一个宏任务执行,所以我们通常代码的执行顺序都是从上往下执行。
  • setTimeout 和 setInterval : 设置定时器的回调函数作为宏任务执行。函数内的内容会在设计的时间到了时才会执行,也不会阻挡它下面的代码的执行。
  • setImmediate: 用于 Node.js 环境,将回调函数设置为宏任务。
  • I/O操作:文件请求、网络请求等异步 I/O 操作。
  • UI渲染:在浏览器中,用户界面的渲染也是一个宏任务,确保用户能够及时看到页面的更新。

微任务

微任务是具有较高优先级的任务,会在当前任务执行结束后立即执行。下面我将会介绍一些常见的微任务:

  • Promise的回调函数:then() 方法的回调函数属于微任务,确保异步操作的处理顺序。
  • MutationObserver 的回调:当 DOM 发生变化时,注册的回调函数会被放入微任务队列。
  • Process.nextTick() :在 Node.js 中,process.nextTick() 也是一个微任务。

宏任务和微任务有什么作用?它又被用在哪里呢?

宏任务和微任务的执行顺序

在 JavaScript 中,宏任务和微任务储存在任务队列中。当宏任务执行完毕后,事件循环会检查微任务队列是否为空,如果不为空,则执行微任务队列中的任务。这一过程会不断循环,确保异步任务按照正确的顺序得到执行。这也大致是Event loop的执行顺序。Event loop的执行顺序主要分为以下五步:

    1. 执行同步代码
    1. 当执行栈(用于存储函数调用、变量和上下文信息)为空时,查询是否有异步代码需要执行。
    1. 如果有异步,则执行微任务
    1. 如果有需要,会渲染页面
    1. 执行宏任务(这也叫下一轮Event loop的开启)

JavaScript事件循环机制:深入解析Event loop

宏任务放进宏任务队列中,微任务放进微任务队列中。

下面我们通过一个简单的案例来了解宏任务和微任务的执行顺序。

console.log('开始');

setTimeout(() => {
  console.log('时间结束,执行');
}, 1000);

Promise.resolve().then(() => {
  console.log('Promise 执行');
});

console.log('结束');

结果:

JavaScript事件循环机制:深入解析Event loop

解释:

  • 第一次循环
  1. 第一步:执行同步代码,先后输出“开始”和“结束”。
  2. setTimeout()为异步中的宏任务,放入宏任务队列。
  3. Promise.resolve().then()为异步中的微任务,放入微任务队列。
  4. 第二步:此时执行栈为空,检查是否有异步代码需要执行。
  5. 第三步:如果有异步,则执行微任务队列中的微任务。这里是Promise.resolve().then(),所以输出Promise 执行
  6. 第四步:如果有需要,渲染页面,这里没有,这一步不执行操作。
  7. 第五步:执行宏任务setTimeout(),新的循环开始。
  • 第二次循环
  1. 第一步:执行同步代码,输出 时间结束,执行。 其他四步这里都没有,完成最终结果输出。

所以最后输出的依次是: 开始 结束 Promise 执行 时间结束,执行。

有了这些前面这些内容铺垫,我们可以倒回去看看最开始的那道面试题,会发现还有一个东西需要了解,async是什么?

async函数

async 是 JavaScript 中用于定义异步函数的关键字。异步函数返回一个 Promise 对象,通过 async 关键字,可以更方便地使用异步编程,以及使用 await 来等待异步操作的完成。我们在函数中返回一个Promise函数通常比较繁琐,而async和await关键字让我们可以用一种更简洁的方式写出基于Promise的异步行为,而无需刻意地进行链式调用promise。这可以大大节省代码的书写量。看下面代码就能快速了解async和await的用法。

function A(){
  return new Promise(function(resolve,reject) {
    setTimeout(() => {
      console.log('异步A完成');
      resolve();
    }, 1000);
  })
}

function B(){
  return new Promise(function(resolve,reject) {
    setTimeout(() => {
      console.log('异步B完成');
      resolve();
    }, 500);
  })
}

function C(){
  return new Promise(function(resolve,reject) {
    setTimeout(() => {
      console.log('异步C完成');
      resolve();
    }, 100);
  })
}

//----------------------------------------------------------------
// 这个写法也不太好看
// A()
// .then(() => {   // .then虽然默认会返回promise对象。但是当.then的回调有人为返回promise对象时,.then默认的proimse会失效
//   return B()
// }) 
// .then(() => {
//     C();
// })


//----------------------------------------------------------------
// await 不能单独出现
async function foo(){    // 函数前面加上一个async 相当于 函数内部返回了一个return new Promise(function(resolve,reject) {})
  await A();  // await会阻塞后续代码,将后续代码推入到微任务队列
  // console.log(1); // A()先执行完 , 1再打印
  await B();
  await C();
}

foo()

值得注意的是:正如上述代码注释中所说的“await会阻塞后续代码,将后续代码推入到微任务队列”,await会将其后的代码封装成一个Promise对象,并将其推入微任务队列中。

我们再来看之前的面试题

console.log('script start') 
async function async1() {
  await async2()
  console.log('async1 end') 
}
async function async2() {
  console.log('async2 end') 
}
async1()
setTimeout(function () {
  console.log('setTimeout') 
}, 0)
new Promise(resolve => {
  console.log('Promise') 
  resolve()
})
  .then(function () {
    console.log('promise1') 
  })
  .then(function () {
    console.log('promise2') 
  })
console.log('script end') 

下面我们使用Event loop五部曲来解决上面这题:

第一轮循环

  • 第一步:执行同步代码,先输出script start,async1()函数调用,await async2()会将后面的代码console.log('async1 end')推入微任务队列中,执行async2(),函数内部的console.log('async2 end')为同步代码,直接输出async2 end,然后遇到setTimeout为异步中的宏任务,推入宏任务队列中。new Promise为同步代码,直接执行,输出Promise,而其后接的.then都为异步中的微任务,依次推入微任务队列,此时微任务队列中有 【console.log('promise2'),console.log('promise1'),console.log('async1 end')】(后面为队列的出口)。 宏任务队列中只有【setTimeout(...)】。然后遇到同步代码console.log('script end'),直接执行,输出script end。此时执行栈为空,我们开始第二步。

  • 第二步:查询队列中是否有异步代码需要执行

  • 第三步:如果有,则执行微任务,按进入微任务队列的先后顺序依次输出:async1 end,promise1,promise2.

  • 第四步:如果有需要,渲染页面,这里不需要。

  • 第五步:执行宏任务(这也叫下一轮Event loop的开启)

第二轮循环

  • 第一步:执行同步代码,输出setTimeout
  • 第二步:此时执行栈为空,检查是否有异步代码需要执行。
  • 第三步:发现宏任务队列和微任务队列都没有异步代码需要执行。结束。

最后结果:

JavaScript事件循环机制:深入解析Event loop

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