首页 关于 文章 作品集 工具 联系
返回文章列表

JavaScript 异步编程:从回调到 async/await

JavaScript 是单线程语言,但实际开发中我们几乎无时无刻不在处理异步操作:网络请求、文件读写、定时器、事件监听……从最初的回调地狱,到 Promise 链,再到 async/await 的优雅语法,JavaScript 的异步编程经历了三次重大演进。这篇文章带你完整理解这三个阶段,掌握每种方案的精髓和适用场景。

一、JavaScript 为什么需要异步?

JavaScript 只有一个主线程。如果所有操作都是同步的,一个 3 秒的网络请求会冻结整个页面——UI 无法响应、动画停止、按钮点击无效。异步机制的核心思想是:遇到耗时操作,先让出来,等结果到了再处理

// 同步代码:会阻塞主线程

// 模拟 3 秒的网络请求(同步阻塞)
const end = Date.now() + 3000;
while (Date.now() < end) {}
  // 3 秒后才输出,期间页面卡死

// 异步代码:不阻塞

setTimeout(() => , 3000);  // 3 秒后输出
  // 立即输出,不等待
// 输出顺序:1, 3, 2

二、回调函数(Callback):异步的起点

2.1 基本模式

回调函数是最原始的异步处理方式:把一个函数当作参数传给异步操作,操作完成后调用这个函数。

function fetchData(url, callback) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.onload = function() {
    if (xhr.status === 200) {
      callback(null, JSON.parse(xhr.responseText));
    } else {
      callback(new Error('请求失败: ' + xhr.status));
    }
  };
  xhr.onerror = function() {
    callback(new Error('网络错误'));
  };
  xhr.send();
}

// 使用
fetchData('/api/user', function(err, data) {
  if (err) {
    console.error(err);
    return;
  }
  
});

2.2 回调地狱

当多个异步操作有依赖关系时,回调会层层嵌套,形成难以维护的"回调地狱":

// 回调地狱:可读性极差,难以维护
fetchData('/api/user/1', function(err, user) {
  if (err) return console.error(err);
  fetchData('/api/posts?userId=' + user.id, function(err, posts) {
    if (err) return console.error(err);
    fetchData('/api/comments?postId=' + posts[0].id, function(err, comments) {
      if (err) return console.error(err);
      
      // 如果还要继续嵌套...
    });
  });
});

回调地狱的问题不只是缩进难看,更严重的是:错误处理困难(每层都要 try-catch)、流程控制复杂(并发、串行难以表达)、调试困难(调用栈被异步打断)。

三、Promise:异步编程的救星

3.1 Promise 基础

Promise 是一个代表异步操作最终结果的对象。它有三种状态:

  • pending:初始状态,既没有成功也没有失败
  • fulfilled:操作成功
  • rejected:操作失败
const promise = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('操作成功的结果');  // 将状态变为 fulfilled
    } else {
      reject(new Error('操作失败'));  // 将状态变为 rejected
    }
  }, 1000);
});

promise
  .then(result => )       // "操作成功的结果"
  .catch(error => console.error(error))        // 捕获错误
  .finally(() => );  // 清理工作

3.2 Promise 链式调用

Promise 的 .then() 返回一个新的 Promise,这就是链式调用的基础:

// 用 Promise 链替代回调地狱
fetch('/api/user/1')
  .then(res => res.json())
  .then(user => fetch('/api/posts?userId=' + user.id))
  .then(res => res.json())
  .then(posts => fetch('/api/comments?postId=' + posts[0].id))
  .then(res => res.json())
  .then(comments => )
  .catch(err => console.error('任何一步出错都会被捕获:', err));

3.3 并发控制

Promise 的真正威力在于并发控制:

// Promise.all:所有都成功才成功,一个失败就失败
const [users, posts, comments] = await Promise.all([
  fetch('/api/users').then(r => r.json()),
  fetch('/api/posts').then(r => r.json()),
  fetch('/api/comments').then(r => r.json())
]);

// Promise.allSettled:全部完成(不管成功失败)才返回
const results = await Promise.allSettled([
  fetch('/api/users').then(r => r.json()),
  fetch('/api/posts').then(r => r.json()),
  fetch('/api/comments').then(r => r.json())
]);
// results = [{status: 'fulfilled', value: ...}, {status: 'rejected', reason: ...}]

// Promise.race:最快的一个(无论成功失败)
const fastest = await Promise.race([
  fetch('/api/primary').then(r => r.json()),
  fetch('/api/backup').then(r => r.json())
]);

// Promise.any:最快的一个成功结果
const firstSuccess = await Promise.any([
  fetch('/api/server1').then(r => r.json()),
  fetch('/api/server2').then(r => r.json())
]);

3.4 封装异步工具函数

/**
 * 带超时的 fetch
 */
function fetchWithTimeout(url, options = {}, timeout = 5000) {
  return Promise.race([
    fetch(url, options),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('请求超时')), timeout)
    )
  ]);
}

/**
 * 带重试的 fetch
 */
async function fetchWithRetry(url, options = {}, retries = 3, delay = 1000) {
  for (let i = 0; i <= retries; i++) {
    try {
      const res = await fetch(url, options);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return await res.json();
    } catch (err) {
      if (i === retries) throw err;
      await new Promise(r => setTimeout(r, delay * Math.pow(2, i))); // 指数退避
    }
  }
}

/**
 * 串行执行(按顺序一个接一个)
 */
async function serial(tasks) {
  const results = [];
  for (const task of tasks) {
    results.push(await task());
  }
  return results;
}

/**
 * 并发控制(最多 n 个同时执行)
 */
async function concurrent(tasks, limit = 5) {
  const results = [];
  const executing = new Set();

  for (const [i, task] of tasks.entries()) {
    const p = Promise.resolve().then(() => task());
    results[i] = p;
    executing.add(p);
    p.finally(() => executing.delete(p));
    if (executing.size >= limit) {
      await Promise.race(executing);
    }
  }
  return Promise.all(results);
}

四、async/await:异步的终极形态

4.1 基本语法

async/await 是 Promise 的语法糖,让异步代码看起来像同步代码:

// async 函数总是返回 Promise
async function getUser(id) {
  const response = await fetch(`/api/user/${id}`);
  const data = await response.json();
  return data;
}

// 使用
const user = await getUser(1);  // 看起来像同步代码

4.2 错误处理

// 方式1:try-catch(推荐)
async function loadUserData(userId) {
  try {
    const user = await fetch(`/api/user/${userId}`).then(r => r.json());
    const posts = await fetch(`/api/posts?userId=${userId}`).then(r => r.json());
    return { user, posts };
  } catch (error) {
    console.error('加载用户数据失败:', error);
    // 可以返回默认值,而不是让错误冒泡
    return { user: null, posts: [] };
  }
}

// 方式2:Go 风格的错误处理
function asyncWrapper(promise) {
  return promise.then(data => [data, null]).catch(err => [null, err]);
}

async function loadData() {
  const [users, usersErr] = await asyncWrapper(fetch('/api/users').then(r => r.json()));
  if (usersErr) return console.error(usersErr);

  const [posts, postsErr] = await asyncWrapper(fetch('/api/posts').then(r => r.json()));
  if (postsErr) return console.error(postsErr);

  return { users, posts };
}

4.3 实战:优雅的 API 请求层

class ApiClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
    this.defaultHeaders = {
      'Content-Type': 'application/json',
    };
  }

  setToken(token) {
    this.defaultHeaders['Authorization'] = `Bearer ${token}`;
  }

  async request(method, path, data = null) {
    const url = `${this.baseURL}${path}`;
    const config = {
      method,
      headers: { ...this.defaultHeaders },
    };
    if (data && method !== 'GET') {
      config.body = JSON.stringify(data);
    }

    try {
      const response = await fetch(url, config);
      if (!response.ok) {
        const error = await response.json().catch(() => ({}));
        throw new Error(error.message || `HTTP ${response.status}`);
      }
      return await response.json();
    } catch (error) {
      if (error.name === 'TypeError') {
        throw new Error('网络连接失败,请检查网络');
      }
      throw error;
    }
  }

  get(path)    { return this.request('GET', path); }
  post(path, data) { return this.request('POST', path, data); }
  put(path, data)  { return this.request('PUT', path, data); }
  delete(path)     { return this.request('DELETE', path); }
}

// 使用
const api = new ApiClient('https://api.example.com');
api.setToken(localStorage.getItem('token'));

const users = await api.get('/users');
const newUser = await api.post('/users', { name: '张兴中' });

五、事件循环与执行顺序

理解事件循环(Event Loop)是掌握 JavaScript 异步的关键:



setTimeout(() => , 0);

Promise.resolve().then(() => );



// 输出顺序:1, 4, 3, 2
// 同步代码 → 微任务(Promise.then)→ 宏任务(setTimeout)

执行优先级:同步代码 > 微任务(Promise.then、queueMicrotask)> 宏任务(setTimeout、setInterval、I/O)。每个宏任务执行完后,会清空所有微任务,再执行下一个宏任务。

六、常见陷阱与最佳实践

  • 忘记 await:async 函数中调用 async 函数必须 await,否则得到的是 Promise 对象
  • 循环中的 await:需要串行时在循环内 await,需要并行时用 Promise.all()
  • 未捕获的 Promise:总是添加 .catch() 或在 async 函数中 try-catch
  • 内存泄漏:取消不再需要的请求(AbortController),清理事件监听器
// 请求取消:AbortController
const controller = new AbortController();

async function fetchData() {
  try {
    const res = await fetch('/api/data', {
      signal: controller.signal
    });
    return await res.json();
  } catch (err) {
    if (err.name === 'AbortError') {
      
      return null;
    }
    throw err;
  }
}

// 组件卸载时取消请求
// controller.abort();

异步编程是 JavaScript 的核心能力之一。理解事件循环、熟练使用 Promise 和 async/await,是每个前端开发者的必修课。记住:回调是基础,Promise 是核心,async/await 是日常。选择合适的工具,写出清晰、可维护的异步代码。