(JavaScript)深入理解 JavaScript 事件循环

268 阅读4分钟

引入

JavaScript 是一种单线程的语言,意味着它一次只能执行一个任务。然而,它却能够处理许多并发任务,这得益于它的事件驱动模型事件循环机制,今天将来给大家介绍一下事件循环相关知识

什么是事件循环?

事件循环是 JavaScript 运行时环境中的一个重要组成部分,负责管理执行任务的顺序。它允许 JavaScript 运行时在等待异步任务完成时执行其他任务,以保持程序的响应性。

宏任务(Macro Tasks)

宏任务是由 JavaScript 运行时环境(如浏览器或 Node.js)安排的任务,它们被添加到任务队列中。典型的宏任务包括定时器回调、事件监听器回调、I/O 操作等。在每次事件循环迭代中,宏任务队列中的任务会被依次执行,直到队列为空。

微任务(Micro Tasks)

微任务是在宏任务执行完毕后立即执行的任务,它们不会被添加到任务队列中,而是被添加到微任务队列中。典型的微任务包括 Promise 的回调函数、MutationObserver 回调等。微任务队列的任务会在下一个事件循环迭代开始前执行。

事件循环的过程:

  1. 执行全局同步代码:首先,执行全局同步代码,将同步任务添加到执行栈中,并开始执行它们。
  2. 处理宏任务:当遇到宏任务(例如定时器回调、I/O 操作、事件监听器回调等)时,将其添加到相应的宏任务队列中。
  3. 处理微任务:在执行栈为空时,会首先处理微任务队列中的任务(例如 Promise 的回调函数)。微任务队列的任务会在下一个事件循环迭代开始前执行。
  4. 从宏任务队列中获取任务:如果微任务队列为空,事件循环会从宏任务队列中获取一个宏任务,并将其相关的回调函数推入执行栈中执行。
  5. 重复:重复以上步骤,直到所有任务队列都为空。

代码示例:

console.log('Start');

// 微任务
Promise.resolve().then(function() {
  console.log('Microtask 1');
});

// 宏任务
setTimeout(function() {
  console.log('Timeout 1');
  
  // 微任务
  Promise.resolve().then(function() {
    console.log('Microtask 2');
  });
  
}, 0);

// 宏任务
setTimeout(function() {
  console.log('Timeout 2');
}, 0);

console.log('End');

初始状态

  • 执行栈: 空
  • 宏任务队列: 空
  • 微任务队列: 空

第1步

执行 console.log('Start');,输出 "Start"。

  • 执行栈console.log('Start');
  • 宏任务队列: 空
  • 微任务队列: 空

第2步

执行 Promise.resolve().then(function() { console.log('Microtask 1'); });,微任务入队,但尚未执行。

  • 执行栈Promise.resolve().then(...)
  • 宏任务队列: 空
  • 微任务队列console.log('Microtask 1');

第3步

执行第一个 setTimeout(function() { console.log('Timeout 1'); ... }, 0);,宏任务入队。

  • 执行栈setTimeout(...)
  • 宏任务队列function() { console.log('Timeout 1'); ... }
  • 微任务队列console.log('Microtask 1');

第4步

执行第二个 setTimeout(function() { console.log('Timeout 2'); }, 0);,宏任务入队。

  • 执行栈setTimeout(...)

  • 宏任务队列:

    • function() { console.log('Timeout 1'); ... }
    • function() { console.log('Timeout 2'); }
  • 微任务队列console.log('Microtask 1');

第5步

执行 console.log('End');,输出 "End"。

  • 执行栈console.log('End');

  • 宏任务队列:

    • function() { console.log('Timeout 1'); ... }
    • function() { console.log('Timeout 2'); }
  • 微任务队列console.log('Microtask 1');

第6步

执行栈清空,开始处理微任务队列,执行 console.log('Microtask 1');,输出 "Microtask 1"。

  • 执行栈: 空

  • 宏任务队列:

    • function() { console.log('Timeout 1'); ... }
    • function() { console.log('Timeout 2'); }
  • 微任务队列: 空

第7步

微任务队列清空,事件循环检查宏任务队列,取出并执行第一个宏任务,执行 console.log('Timeout 1');,输出 "Timeout 1"。

  • 执行栈console.log('Timeout 1');
  • 宏任务队列function() { console.log('Timeout 2'); }
  • 微任务队列: 空

第8步

在执行 "Timeout 1" 的宏任务中,再次添加微任务 Promise.resolve().then(function() { console.log('Microtask 2'); });,微任务入队。

  • 执行栈Promise.resolve().then(...)
  • 宏任务队列function() { console.log('Timeout 2'); }
  • 微任务队列console.log('Microtask 2');

第9步

宏任务 "Timeout 1" 执行完毕,执行栈清空,开始处理微任务队列,执行 console.log('Microtask 2');,输出 "Microtask 2"。

  • 执行栈: 空
  • 宏任务队列function() { console.log('Timeout 2'); }
  • 微任务队列: 空

第10步

微任务队列清空,事件循环检查宏任务队列,取出并执行第二个宏任务,执行 console.log('Timeout 2');,输出 "Timeout 2"。

  • 执行栈console.log('Timeout 2');
  • 宏任务队列: 空
  • 微任务队列: 空

第11步

所有任务执行完毕,执行栈清空。

最终输出顺序是:

Start
End
Microtask 1
Timeout 1
Microtask 2
Timeout 2

结语

JavaScript 事件循环机制是理解异步编程的关键。通过深入了解事件循环的工作原理以及宏任务与微任务的区别,我们可以更好地处理异步任务,提高程序的性能和响应性。希望本文能够帮助你更深入地理解 JavaScript 的事件循环机制!

作者的小猜想(未必准确,有错必改)

js中定时器我理解是回调定时加入宏任务队列,且没有先后这一说,如果队列里面有其他宏任务,新的回调也是加入到队尾,所以说“定时未必准时”,因为处理定时回调前可能微任务队列,宏任务队列还有其他的任务要处理,当然一般情况下应该都是准时的。