JavaScript 异步编程与 Promise 详解

49 阅读3分钟

JavaScript 异步编程与 Promise 详解

在 JavaScript 开发中,理解异步机制是掌握现代前端和 Node.js 编程的关键。本文将从执行顺序、事件循环、回调函数到 Promise 的使用,系统梳理 JavaScript 的异步处理方式。

同步与异步的执行顺序

JavaScript 是一门单线程语言,这意味着它一次只能执行一个任务。代码默认按照书写顺序同步执行:

console.log(1);
setTimeout(() => {
  console.log(2);
}, 3000);
console.log(3);

输出结果为:

1
3
(3秒后)2

原因在于 setTimeout异步操作,它不会阻塞主线程。JS 引擎会将其回调函数放入任务队列(Task Queue) ,等到当前所有同步代码执行完毕,并且定时器时间到达后,才由事件循环(Event Loop) 将其推入调用栈执行。

使用 Promise 实现“异步同步化”

虽然异步不阻塞主线程,但有时我们希望控制多个异步操作的执行顺序。ES6 引入的 Promise 提供了一种更优雅的流程控制方式。

console.log(1);

const p = new Promise((resolve) => {
  setTimeout(() => {
    console.log(2);
    resolve(); // 标记 Promise 为 fulfilled 状态
  }, 3000);
});

p.then(() => {
  console.log(3);
});

console.log(4);

输出结果:

1
4
(3秒后)2
(紧接着)3
  • Promise 构造函数中的执行器(executor)是同步立即执行的。
  • .then() 中的回调会在 Promise 状态变为 fulfilled 后,被放入微任务队列(Microtask Queue),在当前宏任务结束后、下一次事件循环前执行。

Node.js 中的异步 I/O 与 Promise 封装

在 Node.js 中,文件读取等 I/O 操作也是异步的:

import fs from 'fs';

console.log(1);

// 原生异步回调:读取 a.txt
fs.readFile('./a.txt', (err, data) => {
  if (!err) {
    console.log(data.toString());
  }
});

// 使用 Promise 封装 b.txt 的读取
const p = new Promise((resolve, reject) => {
  console.log(3); // 同步执行
  fs.readFile('./b.txt', (err, data) => {
    if (err) {
      reject(err); // 若 b.txt 不存在,此处会 reject
    } else {
      resolve(data.toString());
    }
  });
});

p.then(data => {
  console.log(data, '//////');
}).catch(err => {
  console.log(err, '文件读取失败了');
});

console.log(2);

假设项目目录结构如下:

project/
├── a.txt     ← 存在,内容为 "hello world"
└── (b.txt 可能不存在)

那么程序输出可能为:

1
3
2
hello world
[Error: ENOENT: no such file or directory, open './b.txt'] 文件读取失败了

关键说明

  • a.txt 存在,因此第一个 readFile 成功打印内容。
  • b.txt 不存在fs.readFile 会传入非空 err 对象,Promise 被 reject,从而触发 .catch()
  • 这体现了 Promise 对异步错误的统一捕获能力——即使错误发生在未来的异步回调中,也能被 .catch 捕获。

通过 Promise 包装回调式 API,我们避免了深层嵌套(“回调地狱”),并实现了清晰的成功/失败分离处理

浏览器中的异步请求:Fetch 与 Promise

在浏览器环境中,网络请求(如 fetch)天然返回 Promise:

<ul id="members"></ul>
<script>
  fetch("https://api.github.com/orgs/lemoncode/members")
    .then(response => response.json())
    .then(members => {
      document.getElementById('members').innerHTML = 
        members.map(item => `<li>${item.login}</li>`).join('');
    })
    .catch(err => console.error('请求失败:', err));
</script>
  • fetch() 返回一个 Promise,需调用 .json() 解析响应体(它也返回 Promise)。
  • 使用 .then 链式处理数据流,.catch 统一处理网络错误或解析异常。

核心概念总结

1. 单线程与事件循环

  • JavaScript 主线程负责执行同步代码。
  • 异步任务(定时器、I/O、网络)由宿主环境处理,完成后回调进入任务队列。
  • 事件循环协调调用栈与任务队列,确保异步回调最终被执行。

2. 宏任务 vs 微任务

  • 宏任务(Macrotask)setTimeout、I/O、UI 渲染等。
  • 微任务(Microtask)Promise.then/catchqueueMicrotask
  • 执行顺序:当前宏任务 → 清空所有微任务 → 渲染 → 下一个宏任务。

3. Promise 的状态机

  • pending → 初始状态
  • fulfilled → 调用 resolve(value)
  • rejected → 调用 reject(reason)
  • 状态一旦改变,不可逆;.then() 返回新 Promise,支持链式调用。

通过合理使用 Promise,我们可以将复杂的异步逻辑转化为清晰、可维护的代码结构,为后续使用 async/await 奠定坚实基础。理解这些底层机制,是写出高性能、健壮异步代码的前提。