event loop

前言

JavaScript 是一种单线程的编程语言,其执行机制依赖于事件循环。事件循环(Event Loop)是 JavaScript 引擎的一部分,负责监控调用栈(Call Stack)和任务队列(Task Queue),检查是否有任务需要执行。当调用栈为空时,事件循环会从任务队列中取出一个任务并放入调用栈执行,这个过程不断重复,形成了循环。

进程与线程

在深入了解 JavaScript 的事件循环之前,我们需要先理解进程(Process)和线程(Thread)的基本概念。

进程 是一个程序在操作系统中执行的实例。每个进程都有自己独立的内存空间、系统资源和一个主线程。操作系统通过进程管理系统资源的分配。

线程 是进程内的一个执行单元,也是 CPU 调度的基本单位。一个进程可以包含多个线程,这些线程共享进程的内存空间和资源,但每个线程有自己的栈、寄存器和程序计数器。

进程和线程的区别

  • 独立性:进程之间是独立的,而线程共享进程的资源。

  • 内存空间:进程有独立的内存空间,线程共享进程的内存空间。

  • 资源开销:创建和销毁进程的开销较大,而创建和销毁线程的开销较小。

  • 通信方式:进程间通信需要借助操作系统提供的机制(如管道、消息队列等),线程间通信可以通过共享内存直接进行。

JavaScript 是单线程

工作流程

JavaScript 是单线程语言,这意味着同一时间只能执行一个任务。单线程模型简化了编程模型,因为开发者不需要处理多线程带来的复杂问题,如资源竞争和死锁。

JavaScript 的单线程特性通过调用栈和事件循环来管理任务的执行顺序。调用栈是一个 LIFO(后进先出)的数据结构,用于存储函数调用。每当一个函数被调用时,它就会被推入调用栈顶,当函数执行完毕时,它会从调用栈顶弹出。

优点

  1. 节约性能

    • 单线程意味着不需要处理线程同步、锁机制等复杂的多线程操作,避免了多线程编程中的潜在竞态条件和死锁问题。
    • 在一些情况下,单线程的管理能力更高效,因为不需要额外的线程切换和上下文切换开销。
  2. 简化编程模型

    • JavaScript 的单线程模型简化了并发编程的复杂性,使得开发者可以更专注于业务逻辑的实现,而不用过多关注线程安全、同步等问题。
  3. 避免资源竞争

    • 单线程执行避免了多线程中的资源竞争问题,如共享内存的并发读写问题,从而减少了一些潜在的 Bug 和调试难度。

同步与异步代码

同步代码 是按顺序逐行执行的代码。当一行代码在执行时,后续的代码必须等待其完成。这种方式简单但可能导致阻塞,尤其是在执行 I/O 操作或复杂计算时。

异步代码 允许代码在等待某些操作完成时,继续执行其他任务。异步操作通常通过回调函数、Promise 或 async/await 实现,避免了阻塞主线程。

同步代码示例

js 复制代码
function syncOperation() {
  console.log('Start of operation');
  // 模拟一个耗时1秒的操作
  const start = Date.now();
  while (Date.now() - start < 1000) {}
  console.log('End of operation');
}

console.log('Before sync operation');
syncOperation();
console.log('After sync operation');


// Before sync operation
// Start of operation
// End of operation
// After sync operation

异步代码示例

js 复制代码
function asyncOperation(callback) {
  console.log('Start of async operation');
  setTimeout(() => {
    console.log('End of async operation');
    callback();
  }, 1000);
}

console.log('Before async operation');
asyncOperation(() => {
  console.log('Async operation completed');
});
console.log('After async operation');


// Before async operation
// Start of async operation
// After async operation
// End of async operation
// Async operation completed

宏任务与微任务

在 JavaScript 的事件循环中,任务可以分为两类:宏任务(Macro Task)和微任务(Micro Task)。

宏任务

  • setTimeout

  • setInterval

  • setImmediate

  • I/O 操作

  • UI 渲染

微任务

  • Promise.then

  • Promise.catch

  • Promise.finally

  • process.nextTick

  • MutationObserver

事件循环执行顺序

事件循环的执行顺序是理解 JavaScript 异步行为的关键。以下是事件循环的详细执行步骤:

  1. 执行同步代码:所有同步代码都会在主线程上依次执行,直至调用栈为空。

  2. 同步代码执行完毕,检测是否有异步需要执行:当调用栈为空时,事件循环会检查是否有异步任务需要执行。

  3. 检测到异步任务,先执行微任务:微任务队列中的所有任务会被依次执行,直到微任务队列为空。

  4. 微任务执行完毕,如果有需要就会渲染页面:在浏览器环境中,渲染引擎会在微任务执行完毕后判断是否需要更新 UI,并进行页面渲染。

  5. 执行异步宏任务,开启下一次事件循环:最后,事件循环会从宏任务队列中取出一个任务执行,然后开始下一次事件循环。

示例代码

js 复制代码
console.log("script start");

async function async1() {
  await async2();

  //await 会将后续的代码阻塞进微任务队列
  console.log("async1 end");
}
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");

详解

  • 同步代码部分

    • console.log("script start"); 输出 "script start"。
  • 定义异步函数 async1 和 async2

    • 这两行只是定义函数,不会立即执行它们的内容。
  • 调用 async1

    • 进入 async1,执行 await async2()
    • 进入 async2,执行 console.log("async2 end"); 输出 "async2 end"。
    • async2 执行完毕,返回一个已解决的 Promise,await 使得 async1 的后续代码被推入微任务队列。
  • 设置 setTimeout

    • setTimeout 被调用,注册一个回调函数,并将其放入宏任务队列。该函数将在所有同步和微任务执行完毕后执行。
  • Promise

    • new Promise 立即执行,输出 "promise"。
    • resolve() 被调用,then 回调被推入微任务队列。
  • 输出 "script end"

    • console.log("script end"); 输出 "script end"。

结果

相关推荐
zengyuhan5033 小时前
Windows BLE 开发指南(Rust windows-rs)
前端·rust
醉方休3 小时前
Webpack loader 的执行机制
前端·webpack·rust
前端老宋Running3 小时前
一次从“卡顿地狱”到“丝般顺滑”的 React 搜索优化实战
前端·react.js·掘金日报
隔壁的大叔3 小时前
如何自己构建一个Markdown增量渲染器
前端·javascript
用户4445543654263 小时前
Android的自定义View
前端
WILLF3 小时前
HTML iframe 标签
前端·javascript
枫,为落叶3 小时前
Axios使用教程(一)
前端
小章鱼学前端3 小时前
2025 年最新 Fabric.js 实战:一个完整可上线的图片选区标注组件(含全部源码).
前端·vue.js
ohyeah3 小时前
JavaScript 词法作用域、作用域链与闭包:从代码看机制
前端·javascript
流星稍逝3 小时前
手搓一个简简单单进度条
前端