JavaScript 异步编程入门

JavaScript 是单线程的——同一时间只能做一件事。但网页需要同时处理很多事情:等待服务器响应、读取文件、定时提醒。异步编程让 JavaScript 在不阻塞页面的情况下处理这些耗时操作。

为什么需要异步

来看一个最简单的异步操作:setTimeout()

javascript
document.write('开始' + '<br>');

setTimeout(() => {
  document.write('2 秒后执行' + '<br>');
}, 2000);

document.write('结束' + '<br>');

// 输出顺序:开始 → 结束 → 2 秒后执行

setTimeout 不会阻塞后面的代码——这就是异步。“开始”和”结束”立即打印,回调函数在 2 秒后才执行。

setTimeout()setInterval()

javascript
// 延迟执行一次
const timeoutId = setTimeout(() => {
  document.write('3 秒后执行一次' + '<br>');
}, 3000);

// 每秒重复执行,3 秒后自动停止
let count = 0;
const intervalId = setInterval(() => {
  count++;
  document.write('每秒执行一次' + '<br>');
  if (count === 3) {
    clearInterval(intervalId);
    clearTimeout(timeoutId);
    document.write('定时器已清除' + '<br>');
  }
}, 1000);

回调函数

最初处理异步的方式是回调函数:

javascript
function fetchData(callback) {
  setTimeout(() => {
    const data = { name: '张三', age: 25 };
    callback(data);
  }, 1000);
}

fetchData((data) => {
  document.write('收到数据:' + JSON.stringify(data) + '<br>');
});

但当多个异步操作需要按顺序执行时,回调嵌套会迅速恶化:

javascript
// ❌ 回调地狱(Callback Hell)
step1((result1) => {
  step2(result1, (result2) => {
    step3(result2, (result3) => {
      step4(result3, (result4) => {
        console.log('终于完成');
      });
    });
  });
});

这就是为什么 Promise 被发明了。

Promise — 承诺

Promise 代表一个未来会完成的异步操作。它有三种状态:

  • pending(进行中)— 初始状态
  • fulfilled(已成功)— 操作成功
  • rejected(已失败)— 操作失败

创建 Promise

javascript
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('成功了');
    } else {
      reject('失败了');
    }
  }, 1000);
});

promise
  .then((result) => {
    document.write('成功:' + result + '<br>');
  })
  .catch((error) => {
    document.write('失败:' + error + '<br>');
  })
  .finally(() => {
    document.write('无论成败都会执行' + '<br>');
  });

消费 Promise

javascript
promise
  .then((result) => {
    console.log('成功:', result);
  })
  .catch((error) => {
    console.log('失败:', error);
  })
  .finally(() => {
    console.log('无论成败都会执行');
  });

Promise 链

then 可以链式调用,解决回调地狱:

javascript
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function fetchUser(id) {
  return delay(500).then(() => ({ id, name: '张三' }));
}

function fetchPosts(userId) {
  return delay(500).then(() => [{ id: 1, title: '文章1' }]);
}

function fetchComments(postId) {
  return delay(500).then(() => [{ id: 1, text: '写得不错' }]);
}

fetchUser(1)
  .then(user => fetchPosts(user.id))
  .then(posts => fetchComments(posts[0].id))
  .then(comments => document.write('评论:' + JSON.stringify(comments) + '<br>'))
  .catch(error => document.write('出错:' + error + '<br>'));

扁平、可读、错误在链尾统一处理。

async / await — 异步同步写法

ES2017 引入的语法糖,让异步代码看起来像同步代码

javascript
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function fetchUser(id) {
  return delay(500).then(() => ({ id, name: '张三' }));
}

function fetchPosts(userId) {
  return delay(500).then(() => [{ id: 1, title: '文章1' }]);
}

function fetchComments(postId) {
  return delay(500).then(() => [{ id: 1, text: '写得不错' }]);
}

async function loadData() {
  try {
    const user = await fetchUser(1);
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts[0].id);
    document.write('评论:' + JSON.stringify(comments) + '<br>');
  } catch (error) {
    document.write('出错:' + error + '<br>');
  }
}

loadData();
  • async 标记函数为异步函数——它始终返回一个 Promise
  • await 等待一个 Promise 完成,获取其结果
  • try...catch 捕获错误,替代 .catch()

async/await 是当前 JavaScript 异步编程的推荐方式——代码清晰易读,错误处理自然。await 只能在 async 函数内部使用;在顶层(模块顶层)也可以使用,现代浏览器都支持。

并行的异步操作

当多个异步操作互不依赖时,可以并行执行:

javascript
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function fetchUser(id) {
  return delay(800).then(() => ({ id, name: '张三' }));
}

function fetchPosts(userId) {
  return delay(800).then(() => [{ id: 1, title: '文章1' }]);
}

function fetchSettings(userId) {
  return delay(800).then(() => ({ theme: 'dark' }));
}

async function compare() {
  const t1 = Date.now();
  const user = await fetchUser(1);
  const posts = await fetchPosts(2);
  const settings = await fetchSettings(3);
  document.write('串行耗时:' + (Date.now() - t1) + 'ms' + '<br>');

  const t2 = Date.now();
  const [user2, posts2, settings2] = await Promise.all([
    fetchUser(1),
    fetchPosts(2),
    fetchSettings(3),
  ]);
  document.write('并行耗时:' + (Date.now() - t2) + 'ms' + '<br>');
}

compare();

Promise.all 是”全部成功或一个失败”——只要其中一个 Promise 被 reject,整个 Promise.all 立即失败。如果需要”部分成功也可以”的语义,使用 Promise.allSettled

fetch() — 发送网络请求

fetch() 是浏览器内置的 HTTP 请求方法,返回 Promise:

javascript
async function getUser(id) {
  const response = await fetch(`https://api.example.com/users/${id}`);
  if (!response.ok) {
    throw new Error(`请求失败:${response.status}`);
  }
  const data = await response.json(); // 解析 JSON
  return data;
}

fetch 的详细用法在 JSON 章节中进一步展开。