JavaScript 是一门单线程语言,这意味着在同一时间内,它只能做一件事情。如果遇到一个耗时很长的操作(比如网络请求、文件读写),程序就必须等待这个操作完成后才能继续执行下一步,这会导致页面“假死”,用户体验极差。

为了解决这个问题,JavaScript 引入了异步编程模型。

TIPS:对前端来说,JavaScript 单线程的异步模型已经足够处理网络请求等任务。因为 I/O 操作实际上是交给浏览器其他线程完成的,JavaScript 主线程只需在结果返回时再处理,因此不需要像多线程编程那样人为地去管理线程。

1. 同步 (Synchronous) vs 异步 (Asynchronous)

a. 同步编程

代码按照从上到下的顺序依次执行,后一个任务必须等待前一个任务执行完毕才能开始。

console.log("任务 1:开始");
// 这是一个耗时的同步操作
// 在真实场景中,可能是复杂计算或同步的 API 调用
for (let i = 0; i < 1e9; i++) {
// 模拟耗时
}
console.log("任务 2:一个耗时的操作完成");
console.log("任务 3:结束");
// 输出顺序:
// 任务 1:开始
// (等待几秒钟)
// 任务 2:一个耗时的操作完成
// 任务 3:结束

在同步模式下,“任务 3” 必须等待那个耗时的 for 循环结束后才能被打印。

b. 异步编程

当遇到一个耗时操作时,程序不会等待它完成,而是立即开始执行下一个任务。耗时操作会在未来的某个时刻完成后,通过某种机制(如回调函数)来通知主程序。

setTimeout 是一个典型的异步 Web API。

console.log("任务 1:开始");
// 这是一个异步操作
setTimeout(function () {
// 这个函数被称为“回调函数”
console.log("任务 2:一个异步操作完成");
}, 2000); // 2000 毫秒后执行
console.log("任务 3:结束");
// 输出顺序:
// 任务 1:开始
// 任务 3:结束
// (等待 2 秒后)
// 任务 2:一个异步操作完成

在这个例子中,setTimeout 被调用后,JavaScript 引擎并不会等待 2 秒,而是立即执行后面的 console.log("任务 3:结束")。它把回调函数(打印 “任务 2” 的那个函数)交给了浏览器内核去计时,2 秒后当计时完成,这个回调函数才会被放回 JavaScript 的任务队列中等待执行。


2. 回调函数 (Callbacks)

回调函数是实现异步编程最基础的方式。一个回调函数就是被作为参数传递给另一个函数,并在那个函数执行完毕后被调用的函数。

上面的 setTimeout 就是一个典型的例子。在实际开发中,我们经常用它来处理网络请求。

function fetchData(url, callback) {
console.log("正在从 " + url + " 获取数据...");
// 模拟网络请求的延迟
setTimeout(() => {
const data = { userId: 1, content: "这是一些数据" };
console.log("数据获取成功!");
callback(data); // 成功后,调用回调函数并传入数据
}, 1500);
}
// 定义一个回调函数来处理获取到的数据
function handleData(data) {
console.log("处理数据:", data);
}
// 调用 fetchData,并传入回调函数
fetchData("https://api.example.com/data", handleData);
// 输出:
// 正在从 https://api.example.com/data 获取数据...
// (等待 1.5 秒)
// 数据获取成功!
// 处理数据: { userId: 1, content: '这是一些数据' }

3. 回调地狱 (Callback Hell)

当多个异步操作相互依赖时,如果继续使用回调函数,就容易形成层层嵌套的结构,代码难以阅读和维护。这就是所谓的“回调地狱”。

假设我们需要执行三个连续的异步请求:

  1. 获取用户信息。
  2. 根据用户信息获取他的帖子列表。
  3. 根据帖子列表获取第一篇帖子的评论。
// 伪代码示例
fetchUser(
"user123",
function (user) {
console.log("获取到用户:", user);
fetchPosts(
user.id,
function (posts) {
console.log("获取到帖子:", posts);
fetchComments(
posts[0].id,
function (comments) {
console.log("获取到评论:", comments);
// 如果还有下一步...
// fetchSomethingElse(comments[0].id, function(result) {
// // ...
// });
},
function (error) {
// fetchComments 的错误处理
console.error("获取评论失败:", error);
},
);
},
function (error) {
// fetchPosts 的错误处理
console.error("获取帖子失败:", error);
},
);
},
function (error) {
// fetchUser 的错误处理
console.error("获取用户失败:", error);
},
);

这种代码结构:

  • 可读性差:横向扩展,形成“金字塔”形状。
  • 难以维护:逻辑耦合紧密,修改困难。
  • 错误处理复杂:每个异步操作都需要单独的错误处理回调,容易遗漏。

为了解决回调地狱的问题,ES6 引入了 Promise 对象。