likes
comments
collection
share

探索JavaScript事件循环机制:单线程下的异步奥秘前言 在JavaScript中,事件循环(Event Loop)

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

前言

在JavaScript中,事件循环(Event Loop)是一个至关重要的概念,它负责处理异步操作、用户交互(如点击事件)和定时器(如setTimeoutsetInterval)等。事件循环机制使得JavaScript能够非阻塞地执行代码,即使某些操作(如网络请求或定时器)需要花费一些时间才能完成。本文将系统性地探索这些核心概念,从JavaScript的单线程特性出发,在了解同步与异步代码之后深入分析宏任务与微任务的区别,再全面理解事件循环机制的工作原理,并通过面试题练习加深对这些概念的理解与应用。

JavaScript的单线程特性

JavaScript设计之初的主要目的是在网页上添加交互功能,以增强网页的交互性和动态性,所以为了简化并发问题、提高执行效率和避免浏览器环境的限制,开发人员选择了单线程执行模型。它的优点在于:

  1. 节约性能/内存
  2. 节约上下文切换的时间
  3. 减少并发问题(如死锁等)的发生

当然单线程执行也存在一些局限性。例如,它不能充分利用多核CPU的并行处理能力,可能导致某些计算密集型任务的执行效率较低。为了解决这个问题,JavaScript通过一些技术如事件循环机制、Promise、async/await等来支持并发和并行处理。这些技术能让JavaScript代码在执行耗时任务时,不阻塞主线程的执行,从而提高了整体性能。

宏任务与微任务的区别

在讲解宏任务与微任务之前,我们首先要了解一下同步代码与异步代码的区别:

  • 同步代码:每一行代码都按顺序执行,上一行执行完毕才能执行下一行。同步代码不耗时或耗时很短,可以在短时间内完成执行。
  • 异步代码:当遇到耗时代码时,可以允许后续代码继续执行,等到耗时结束后,再回到原来的代码继续执行。JavaScript 中常见的异步操作包括网络请求、文件读写、定时器(setTimeout、setInterval)等。

接下来我们要谈的宏任务和微任务就是异步代码的两个分类,微任务设计用于即时响应的轻量级操作,确保关键路径的高效执行。 宏任务则更适合处理耗时或定时触发的任务,维持程序的整体节奏。接下来是本文的一个知识重点,需要读者牢记,在文章后续考题练习部分我们也会多次使用到这些知识点:

  • 微任务:promise.then(), process.nextTick(), MutationObserver()(监视器,监测DOM结构变化)
  • 宏任务:script, setTimeout(), setInterval(), setImmediate(), I/O(输入输出), UI-rendering(页面渲染)

事件循环机制

事件循环是JavaScript协调同步与异步代码执行的机制,确保单线程环境下的高效运行。

event-loop过程:

  1. 执行同步代码(这属于宏任务)
  2. 同步执行完毕后,检查是否有异步要执行
  3. 执行所有的微任务
  4. 微任务执行完毕后,如果有需要就会渲染页面
  5. 执行异步宏任务,也是开启下一次事件循环

面试中有关题型的考题练习

在面试中,考官可能会手写一段代码给你,让你分析出该代码执行后的打印顺序,以上都是纸上谈兵,实践才能更好掌握,接下来就让我们结合这些知识点来实战一下吧O(∩_∩)O,以下题型由易到难,请动手分析吧。

第一题

console.log(1);
new Promise((resolve, reject) => {
    console.log(2);
    resolve()
})
.then(() => {
    console.log(3);
})
.then(() => {
    console.log(4);
})

setTimeout(() => {
    console.log(5);
}, 0)

console.log(6);

分析:

因为v8是单线程,所以遇到异步代码会先放到微任务栈或者宏任务队列中,等同步代码执行完毕再处理。根据event-loop过程,v8从上往下执行,第一行是同步代码,可以立即执行输出1,接下来是一个函数的调用也是同步,可以执行输出2。第6行代码是then方法,属于微任务所以存入微任务队列中。第9行同样属于微任务存入微任务队列。13行的定时器setTimeout属于宏任务,存入宏任务队列中。第17行属于同步代码,立即执行输出6。

现在同步代码已经执行完毕,接下来执行微任务,微任务队列中有两个then方法,队列遵循先进先出原则,所以先执行第7行打印3,再执行第10行打印4。

微任务执行完毕后,如果有需要就会渲染页面,此处没有页面,所以执行宏任务进入下一次事件循环。setTimeout方法中可能又存在同步代码,微任务,宏任务,还是要按照时间循环机制继续执行下去,但是在这份代码中setTimeout定时器中只有一个同步代码,所以直接打印输出5。

所以打印顺序为: 1 2 6 3 4 5

第二题

console.log(1);
new Promise((resolve, reject) => {
  console.log(2);
  resolve()
})
.then(() => {
  console.log(3);
  setTimeout(() => {
    console.log(4);
  }, 0)
})

setTimeout(() => {
  console.log(5);
  setTimeout(() => {
    console.log(6);
  }, 0)
}, 0)

console.log(7);

分析:

第一行同步代码,输出1,第三行也是同步代码输出2。第6行微任务存入队列中,第13行宏任务存入宏任务队列中,第20行同步代码输出7。

同步代码执行完毕,微任务队列中有一个then方法可以执行,第7行同步代码执行输出3。第8行是宏任务存入队列中,该微任务执行完毕。

执行宏任务,14行是同步代码执行输出5,第15行是宏任务存入队列中。此时同步代码与微任务栈中代码都执行完毕,所以继续执行宏任务,按照先进先出原则,先执行第8行宏任务输出4,最后执行15行宏任务输出6。

所以打印顺序为:1 2 7 3 5 4 6

第三题

在分析此代码执行结果之前,我们要先知道一个知识点:立即执行函数中的 await 会将后续代码阻塞进微任务队列中。它的原理是async函数总是返回一个Promise。当我们在async函数中使用await时,实际上是在等待一个Promise解决或拒绝。当Promise解决或拒绝时,其关联的回调函数(.then().catch()中的代码)会被放入微任务队列中。事件循环会在完成当前宏任务后检查微任务队列,并执行其中的所有任务。

接下来请根据这个知识点继续分析吧ヾ(◍°∇°◍)ノ゙

console.log('script start');

async function async1() {   // 没有调用
  await async2()
  console.log('async1 end');    // 被await阻塞进微任务队列
}
async function async2() {
  console.log('async2 end');
}

async1()

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

new Promise(function(resolve, reject) {
  console.log('promise');
  resolve()
})
.then(() => {
  console.log('then1');
})
.then(() => {
  console.log('then2');
})

console.log('script end');

分析:

正确执行顺序:(await 会将后续代码阻塞进微任务队列)

  • script start
  • async2 end
  • promise
  • script end --> 同步代码执行完毕
  • async1 end
  • then1
  • then2 --> 微任务队列执行完毕
  • setTimeout --> 宏任务队列执行完毕

总结

认真阅读完以上内容后,我相信你一定了解透彻JS中的事件循环机制了,当然在这里我给出的面试题目示例还是较为仁慈了,可能在真正的实战中面试官写出的代码更加的复杂,但是亲爱的千万别害怕,我相信对于认真学习完这篇文章的你来说肯定是小case啦。

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