深入理解 JavaScript 同步与异步:从 Event Loop 到 async/await

🔥 深入理解 JavaScript 同步与异步:从 Event Loop 到 async/await

本文将带你彻底搞懂 JavaScript 的执行机制,掌握同步与异步的核心原理,以及如何使用 Promise 和 async/await 优雅地控制异步流程。

前言

作为一名前端开发者,你是否曾经困惑过:

  • 为什么 JavaScript 需要异步?同步写完不行吗?
  • 为什么 setTimeout 的回调不会阻塞后续代码执行?
  • 为什么 Promise 能让异步代码像同步一样优雅?
  • Event Loop 到底是什么?

今天,我们就来彻底搞懂这些问题!


一、进程与线程:理解并发的基础

在深入 JavaScript 之前,我们先来理解两个核心概念:

1.1 进程(Process)

进程是资源分配的基本单位。每个程序运行时,操作系统会为其分配独立的内存空间和系统资源。

1.2 线程(Thread)

线程是代码执行的基本单位。一个进程可以包含多个线程,它们共享进程的资源。

markdown 复制代码
进程(董事长)── PID
    └── 线程(经理)── TID
        ├── 主线程
        └── 子线程

1.3 多线程 vs 单线程

特性 多线程(C++/Java) 单线程(JavaScript)
执行效率 高(并发执行) I/O 密集型不低,CPU 密集型受限
复杂度 高(需要处理同步锁) 低(简单易懂)
适用场景 系统级应用 网页交互

JavaScript 被设计为单线程语言,这是因为网页交互场景相对简单,单线程足以应对,同时避免了复杂的线程同步问题。


二、JavaScript 的执行机制

2.1 代码执行流程

当我们运行一段 JavaScript 代码时:

  1. 启动进程:操作系统分配资源(PID)
  2. 创建主线程:JavaScript 引擎创建唯一的主线程
  3. 执行同步代码:快速执行完所有同步任务
  4. 处理异步任务:耗时任务交给浏览器的其他线程处理,完成后回调进入任务队列,由 Event Loop 调度执行
javascript 复制代码
// 同步代码 sync
console.log('start');

// 异步代码 async(交给浏览器定时器线程,到期后回调进入任务队列)
setTimeout(() => {
  console.log('222');
}, 1000);

console.log('end');

// 输出顺序:start → end → 222

2.2 同步与异步的本质区别

同步(Sync):代码按顺序执行,前一个任务完成才会执行下一个。

javascript 复制代码
let a = 1;
let b = 2;
let c = 3;
console.log(a + b + c); // 6

异步(Async):耗时任务被跳过,先执行后续同步代码,等同步代码执行完后再处理。

2.3 为什么需要异步?

这是最关键的问题。我们先看一个场景:

javascript 复制代码
// 假设网络请求需要 3 秒
const data = fetch('/api/users');  // 假设这是同步的
console.log(data);

// 在这 3 秒内,页面完全卡死:
// ❌ 用户点击按钮 ------ 没反应
// ❌ 动画停止播放
// ❌ 输入框无法输入
// ❌ 整个页面白屏或"未响应"

根本原因:JavaScript 是单线程的。

单线程意味着同一时刻只能做一件事。如果用同步方式处理耗时操作(网络请求、大量计算、定时器),主线程就会被阻塞,后续所有代码都得排队等着。

css 复制代码
同步模式(阻塞):
主线程:[====网络请求 3 秒====][==后续代码==]
         ↑ 这 3 秒页面完全卡死

异步模式(非阻塞):
主线程:[注册回调][后续代码继续执行...]
浏览器网络线程:    [====网络请求 3 秒====]
                                     ↓
任务队列:                        [回调入队]
                                     ↓
Event Loop:                   [取出回调执行]

异步的本质是:把耗时任务交给浏览器/Node.js 的其他线程处理,主线程继续往下执行,等耗时任务完成后再回来执行回调。

对比 同步处理网络请求 异步处理网络请求
用户体验 页面卡死 3 秒 页面流畅,数据回来后更新
CPU 利用率 干等 3 秒,浪费 空闲时间可以做其他事
代码复杂度 简单,但不可用 稍复杂,但体验好

所以结论很简单:在单线程语言中,异步不是可选的,而是必须的。 如果 JavaScript 用同步方式处理所有事情,用户每次点击按钮、每次发请求,页面都会卡住------这显然是不可接受的。


三、Event Loop:异步的核心机制

3.1 什么是 Event Loop?

Event Loop(事件循环)是 JavaScript 处理异步任务的核心机制。它的工作流程:

javascript 复制代码
┌───────────────────────────┐
│         调用栈(Call Stack)│ ← 执行同步代码
└───────────┬───────────────┘
            │ 调用栈清空后
            ▼
┌───────────────────────────┐
│    微任务队列(Microtask)  │ ← Promise.then、MutationObserver
│    全部执行完               │
└───────────┬───────────────┘
            │ 微任务队列清空
            ▼
┌───────────────────────────┐
│    宏任务队列(Macrotask)  │ ← setTimeout、setInterval、I/O
│    取出一个执行             │
└───────────┬───────────────┘
            │ 执行完回到顶部,重新检查微任务
            ▼
         循环继续...

关键区别

  • 微任务(Microtask):优先级高,每个宏任务执行完后,会清空所有微任务
  • 宏任务(Macrotask):优先级低,每次只取一个执行

3.2 微任务 vs 宏任务

JavaScript 的异步任务分为两类,执行优先级不同:

类型 常见 API 执行时机
微任务(Microtask) Promise.then/catch/finallyMutationObserver 每个宏任务执行完后,清空所有微任务
宏任务(Macrotask) setTimeoutsetIntervalI/OUI 渲染 每次 Event Loop 只取一个执行

执行顺序:同步代码 → 清空所有微任务 → 取一个宏任务执行 → 清空所有微任务 → 取一个宏任务 → ...

用一个例子来理解:

javascript 复制代码
console.log('1');              // 同步

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

Promise.resolve().then(() => {
  console.log('3');            // 微任务
});

console.log('4');              // 同步

// 输出顺序:1 → 4 → 3 → 2
// 1. 同步代码:1、4
// 2. 清空微任务:3
// 3. 取一个宏任务:2

为什么 Promise 比 setTimeout 先执行? 因为 Promise 是微任务,优先级高于 setTimeout 这个宏任务。即使 setTimeout 延迟设为 0,也得等微任务队列清空后才能执行。

3.3 JavaScript 中的异步任务类型

  • 定时器setTimeoutsetInterval
  • 网络请求fetchXMLHttpRequest
  • 事件监听clickscroll 等 DOM 事件
  • Promise:ES6 引入的异步解决方案

四、Promise:异步编程的救星

4.1 为什么需要 Promise?

在 Promise 出现之前,我们只能通过回调函数处理异步:

javascript 复制代码
// 回调地狱(Callback Hell)
getData(function(a) {
  getMoreData(a, function(b) {
    getEvenMoreData(b, function(c) {
      // 嵌套越来越深...
    });
  });
});

Promise 的出现让异步代码变得优雅:

javascript 复制代码
getData()
  .then(a => getMoreData(a))
  .then(b => getEvenMoreData(b))
  .then(c => {
    // 清晰的链式调用
  })
  .catch(err => {
    // 统一的错误处理
  });

4.2 Promise 的基本用法

创建 Promise
javascript 复制代码
const p = new Promise((resolve, reject) => {
  // executor 函数:立即执行,是耗时任务的容器
  // 同步执行,内部可以容纳异步任务

  setTimeout(() => {
    // 异步任务成功完成
    resolve('成功的结果');

    // 或者异步任务失败
    // reject('失败的原因');
  }, 2000);
});
使用 Promise
javascript 复制代码
p
  .then((data) => {
    console.log(data); // "成功的结果"
    console.log('异步任务完成');
  })
  .catch((err) => {
    console.log(err); // "失败的原因"
    console.log('异步任务失败');
  })
  .finally(() => {
    console.log('无论成功失败都会执行');
  });

4.3 Promise 的三种状态

状态 说明 触发方式
Pending 进行中 初始状态
Fulfilled 已成功 调用 resolve()
Rejected 已失败 调用 reject()

重要特性:状态一旦改变,就不会再变!

javascript 复制代码
const p = new Promise((resolve, reject) => {
  resolve('第一次'); // 状态变为 Fulfilled
  reject('第二次');  // 无效,状态已经固定
  resolve('第三次'); // 无效,状态已经固定
});

// 只会执行第一个 then
p.then(data => console.log(data)); // "第一次"

五、async/await:更优雅的异步写法

async/await 是 ES2017 引入的语法糖,让异步代码看起来和同步代码一样简洁。

5.1 基本用法

javascript 复制代码
// async 函数始终返回一个 Promise
async function getData() {
  const response = await fetch('/api/users');  // 等待 Promise 完成
  const users = await response.json();         // 再等待下一个 Promise
  return users;  // 自动包装为 Promise.resolve(users)
}

getData().then(users => console.log(users));

5.2 对比回调和 Promise

同一个需求,三种写法:

javascript 复制代码
// ❌ 回调地狱
fetchUser(userId, function(user) {
  fetchOrders(user.id, function(orders) {
    fetchDetails(orders[0].id, function(detail) {
      console.log(detail);
    });
  });
});

// ✅ Promise 链
fetchUser(userId)
  .then(user => fetchOrders(user.id))
  .then(orders => fetchDetails(orders[0].id))
  .then(detail => console.log(detail));

// ✅✅ async/await(最直观)
async function showDetail() {
  const user = await fetchUser(userId);
  const orders = await fetchOrders(user.id);
  const detail = await fetchDetails(orders[0].id);
  console.log(detail);
}

5.3 错误处理

javascript 复制代码
// 用 try/catch 捕获异步错误
async function fetchData() {
  try {
    const response = await fetch('/api/users');
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    const data = await response.json();
    console.log(data);
  } catch (err) {
    console.log('请求失败:', err);
  } finally {
    console.log('请求结束');
  }
}

5.4 并行执行

await 会阻塞后续代码,如果多个请求没有依赖关系,应该并行执行:

javascript 复制代码
// ❌ 串行(总耗时 = 3 秒 + 2 秒 + 1 秒 = 6 秒)
const users = await fetch('/api/users');    // 3 秒
const posts = await fetch('/api/posts');    // 2 秒
const comments = await fetch('/api/comments'); // 1 秒

// ✅ 并行(总耗时 = max(3, 2, 1) = 3 秒)
const [users, posts, comments] = await Promise.all([
  fetch('/api/users'),
  fetch('/api/posts'),
  fetch('/api/comments')
]);

六、实战:手写 sleep 函数

JavaScript 原生没有 sleep 函数,但我们可以用 Promise 实现:

javascript 复制代码
function sleep(t) {
  return new Promise((resolve, reject) => {
    console.log('同步执行');
    setTimeout(() => {
      resolve();
    }, t);
  });
}

// 使用
sleep(2000)
  .then(() => {
    console.log('2秒后执行');
  });

6.1 执行流程解析

  1. sleep(2000) 被调用
  2. executor 函数立即同步执行,打印 "同步执行"
  3. setTimeout 交给浏览器定时器线程处理
  4. sleep 返回一个 Pending 状态的 Promise
  5. 2秒后,setTimeout 的回调执行,调用 resolve()
  6. Promise 状态变为 Fulfilled,触发 .then() 回调

七、fetch 与 Promise

现代浏览器的 fetch API 底层就是基于 Promise:

javascript 复制代码
console.log('start');

fetch("https://api.example.com/users", {
  method: "POST",
})
  .then((response) => {
    console.log('请求成功:', response);
  })
  .catch((err) => {
    console.log('请求失败:', err);
  });

console.log('end');

// 输出顺序:
// 1. start
// 2. end
// 3. 请求成功/失败(取决于网络状况)

关键点fetch 是异步的,不会阻塞后续代码执行!

⚠️ 常见陷阱fetch 只有在网络错误 (如断网、DNS 失败)时才会触发 .catch()。HTTP 状态码 404、500 等不会触发 catch,需要手动判断:

javascript 复制代码
fetch('/api/users')
  .then(response => {
    if (!response.ok) {  // response.ok 为 true 表示状态码 200-299
      throw new Error(`HTTP ${response.status}`);
    }
    return response.json();
  })
  .then(data => console.log(data))
  .catch(err => console.log('请求失败:', err));

八、控制异步执行顺序

在实际开发中,我们经常需要控制多个异步任务的执行顺序。

8.1 顺序执行

javascript 复制代码
// 需求:先获取所有用户,再获取每个用户的详情
fetch('/api/users')
  .then(response => response.json())
  .then(users => {
    return fetch(`/api/users/${users[0].id}`);
  })
  .then(response => response.json())
  .then(userDetail => {
    console.log('用户详情:', userDetail);
  });

8.2 并行执行

javascript 复制代码
// 同时发起多个请求,全部完成后处理
Promise.all([
  fetch('/api/users'),
  fetch('/api/posts'),
  fetch('/api/comments')
])
  .then(([users, posts, comments]) => {
    console.log('所有数据:', users, posts, comments);
  });

九、常见面试题解析

题目 1:代码输出顺序

javascript 复制代码
console.log('1');

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

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

console.log('4');

答案1 → 4 → 3 → 2

解析(参考 3.2 节的微任务/宏任务知识):

  • 同步代码优先执行:14
  • 清空微任务队列:3(Promise.then 是微任务)
  • 取一个宏任务执行:2(setTimeout 是宏任务)

题目 2:Promise 状态不可变

javascript 复制代码
const promise = new Promise((resolve, reject) => {
  resolve('success1');
  reject('error');
  resolve('success2');
});

promise
  .then(res => console.log(res))
  .catch(err => console.log(err));

答案success1

解析 :Promise 状态一旦改变就不可逆,第一个 resolve 后状态固定为 Fulfilled。


总结

概念 核心要点
进程 资源分配单位
线程 代码执行单位
为什么需要异步 单线程 + 同步阻塞 = 页面卡死,异步是必须的
Event Loop 同步代码 → 清空微任务 → 取一个宏任务 → 循环
微任务/宏任务 Promise 是微任务(优先),setTimeout 是宏任务
Promise 异步编程解决方案,三种状态不可逆
async/await Promise 的语法糖,让异步代码像同步一样简洁

掌握这些核心概念,你就能从容应对各种异步编程场景!


参考资料


📝 作者 :ReBound 📅 日期 :2026年6月9日 🔖 标签:#JavaScript #异步编程 #Promise #EventLoop #前端开发

如果这篇文章对你有帮助,欢迎点赞、收藏、评论!你的支持是我创作的最大动力! 🚀

相关推荐
浮生望1 小时前
JavaScript 异步编程核心:从同步阻塞到 Promise 事件循环
javascript·promise
假如让我当三天老蒯2 小时前
暂时性死区是否和闭包是相背的呢(自学用)
前端·javascript
渣波2 小时前
前端开发主页面小技巧
前端·javascript
小林ixn2 小时前
前端必知:JS同步异步与Promise,终于有人讲明白了!
javascript·面试
bonechips2 小时前
JS:同步与异步,从单线程到 Promise 的编程之路
前端·javascript
先吃饱再说2 小时前
为什么 `setTimeout` 会“插队”?JS 事件循环与 Promise 通关笔记
前端·javascript·promise
Web打印2 小时前
打印PDF面单顺序会乱 使用HttpPrinter连接打印机打印PDF快递面单,顺序会乱,请问有没有碰到过这样的问题呢?是怎么解决的
javascript
Web打印2 小时前
Httpprinter 2 、3 升级到 Httpprinter4、5的 注意事项
javascript
如意IT2 小时前
浏览器CDP自动化检测技术-Error和Worker
前端·javascript·自动化·chromium·指纹浏览器