javascript事件循环机制

106 阅读4分钟

前言

JavaScript是单线程的,即在同一时间内只能处理一个线程。但是现实场景中存在很多的异步操作,如网络请求,IO等,为了解决这些异步导致的效率问题,javascript就通过事件循环(Event Loop)机制来实现非阻塞的异步操作。 深入理解事件循环的执行过程有助于更好地掌握JavaScript异步编程,避免一些常见的陷阱,提高代码的性能和可维护性。同时,对事件循环的理解也为优化异步任务的执行顺序提供了思路,确保程序在处理异步任务时能够更为高效和灵活。

什么是事件循环

事件循环是JavaScript运行时环境中处理异步任务的一种机制。它确保代码在执行异步任务时能够保持响应性,而不会阻塞整个程序的执行。JavaScript事件循环包含两个关键组成部分:微任务队列和宏任务队列。

宏任务和微任务

  • 宏任务: 宏任务是一些较为耗时的任务,它们会被放入宏任务队列中等待执行。常见的宏任务包括定时器(setTimeout、setInterval)、事件监听、I/O等。宏任务会在每次事件循环的迭代中最早进入队列的任务执行。
  • 微任务: 微任务是一些需要尽快执行的任务,它们会在当前任务执行完成后立即执行。常见的微任务包括Promise的回调函数、MutationObserver的回调和process.nextTick(Node.js环境)等。微任务队列会在每个宏任务执行结束后清空。

事件循环过程

  1. 执行同步代码: 从宏任务开始,从上到下按顺序执行所有的同步代码,遇到异步任务时,将其回调函数添加到任务队列中,然后继续执行完当前宏任务中的所有同步代码。
  2. 执行微任务: 执行完同步代码后,检查微任务队列,如果有微任务则按顺序执行所有微任务的回调函数。
  3. 执行宏任务: 选择宏任务队列中最早进入队列的任务执行,直至宏任务队列为空。
  4. 重复: 重复以上步骤,不断循环执行,直至程序结束。

事件循环举例

这里举个笔试中常见的题目类型,即代码执行后控制台的输出内容是什么?

代码

<script>
  console.log(1);

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

  new Promise(function (resolve, reject) {
    console.log(3)
    resolve()
  }).then(function () {
    console.log(4);
    setTimeout(function () {
      console.log(5);
    }, 0);
  }).finally(() => {
    console.log(6);
  })

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

  setTimeout(function () {
    console.log(9);
  }, 100);

  new Promise(function (resolve, reject) {
    console.log(10)
    resolve()
  }).then(function () {
    console.log('a');
    console.log(11);
  })
</script>

输出结果

1
3
7
10
4
a
11
6
8
5
9
2 // 3秒后打印

代码循环分析

执行栈调入第一个宏任务:

每一个<script>标签为一个宏任务,机制以宏任务开始,执行同步代码,输出1,3,7,10,此时宏任务队列为:

  // 队一
  setTimeout(function () {
    console.log(2);
  }, 3000);
  
  // 队二
  setTimeout(function () {
    console.log(8);
  }, 0);

  // 队三
  setTimeout(function () {
    console.log(9);
  }, 100);
  

微任务队列为:

  // 队一
  (function () {
    console.log(4);
    setTimeout(function () {
      console.log(5);
    }, 0);
  }).finally(() => {
    console.log(6);
  })
  
  // 队二
  function () {
    console.log('a');
    console.log(11);
  }

同步任务完成,将微任务队列代码全部调入执行栈执行,输出 4 后遇到 setTimeout ,将其放入宏任务队列,遇到 finally 将其放入微任务队列,接着输出 a11,执行栈代码执行完毕,此时宏任务队列为:

  // 队一
  setTimeout(function () {
    console.log(2);
  }, 3000);
  
  // 队二
  setTimeout(function () {
    console.log(8);
  }, 0);

  // 队三
  setTimeout(function () {
    console.log(9);
  }, 100);
  
  // 队四
  setTimeout(function () {
    console.log(5);
  }, 0);

微任务队列为:

  finally(() => {
    console.log(6);
  })

继续将微任务队列中的代码调入执行栈执行,输出 6,至此,第一个宏任务执行结束。

执行栈调入第二个宏任务

当执行栈和和微任务队列都为空时,查看宏任务队列是否存在待执行代码,如果存在,调入执行。此时宏任务队列中还有4个任务排队,此时调入第一个宏任执行,发现设置时间未到,重新入队,至此,执行栈为空,微任务队列为空,宏任务队列变为:

  // 队一
  setTimeout(function () {
    console.log(8);
  }, 0);
  
  // 队二
  setTimeout(function () {
    console.log(9);
  }, 100);

  // 队三
  setTimeout(function () {
    console.log(5);
  }, 0);
  
  // 队四
  setTimeout(function () {
    console.log(2);
  }, 3000);

执行栈循环调入宏任务

重复上述循环,事件循环机制重新从宏任务队列中调入代码到执行栈中执行,遇到可执行的立马执行,不可执行的重新入队,直至全部队列情况。

总结

  1. 事件循环总是从宏任务开始执行同步代码,遇到异步代码按类型放入微任务队列和宏任务队列中;
  2. 同步代码执行完毕,首先查看微任务队列,有代码则调入执行,没有则同理查看宏任务队列;
  3. 重复上述步骤进行循环调度。