JavaScript的异步编程是其核心特性之一,也是理解JavaScript运行机制的关键。下面我从几个方面详细介绍。

一、为什么需要异步编程?

JavaScript 是单线程语言,意味着同一时间只能做一件事。如果没有异步编程,当遇到耗时操作(如网络请求、文件读取、定时器)时,整个程序就会阻塞,导致页面卡死无法响应。异步编程就是为了解决这个问题,让耗时操作在后台执行,不影响主线程继续处理其他任务。

二、异步编程的演进历程

1. 回调函数(Callback)

最早的异步解决方案,将函数作为参数传入,在异步操作完成后执行。

// 定时器回调
setTimeout(() => {
  console.log('2秒后执行');
}, 2000);

// 事件回调
button.addEventListener('click', () => {
  console.log('按钮被点击');
});

// 传统的Ajax请求
function fetchData(callback) {
  // 模拟异步请求
  setTimeout(() => {
    callback('获取到的数据');
  }, 1000);
}

fetchData((data) => {
  console.log(data);
});

☹ 问题:回调地狱
当多个异步操作有依赖关系时,就会出现嵌套过深的问题:

fetchData((data1) => {
  processData(data1, (data2) => {
    validateData(data2, (data3) => {
      saveData(data3, (result) => {
        console.log('最终结果', result);
      });
    });
  });
});

这种代码难以阅读、难以维护,错误处理也很复杂。

2. Promise

Promise 是 ES6 引入的解决方案,表示一个异步操作的最终完成或失败。

// Promise 的基本使用
const promise = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('成功的数据');
    } else {
      reject('失败的原因');
    }
  }, 1000);
});

promise
  .then(result => {
    console.log('成功:', result);
    return result + '处理';
  })
  .then(processed => {
    console.log('处理后的结果:', processed);
  })
  .catch(error => {
    console.log('失败:', error);
  })
  .finally(() => {
    console.log('无论成功失败都会执行');
  });

★ Promise 的关键特性:

状态不可逆:pending → fulfilled 或 pending → rejected,一旦改变就不能再变

链式调用:通过 .then() 返回新的 Promise,解决了回调地狱问题

错误冒泡.catch() 可以捕获链中任意一个 Promise 的错误

// 用 Promise 改写上面的回调地狱
fetchData()
  .then(data1 => processData(data1))
  .then(data2 => validateData(data2))
  .then(data3 => saveData(data3))
  .then(result => console.log('最终结果', result))
  .catch(error => console.error('出错了', error));

☛ Promise 的静态方法:

// Promise.all:所有都成功才成功,一个失败就失败
Promise.all([fetch1(), fetch2(), fetch3()])
  .then(results => console.log('全部成功', results));

// Promise.race:谁先完成就用谁的结果
Promise.race([fetch1(), fetch2()])
  .then(result => console.log('最快的那个', result));

// Promise.allSettled:等待所有都完成,无论成功或失败
Promise.allSettled([fetch1(), fetch2()])
  .then(results => console.log('所有结果', results));

// Promise.any:任意一个成功就成功,全部失败才失败
Promise.any([fetch1(), fetch2()])
  .then(result => console.log('第一个成功的', result));

3. Generator 函数(中间过渡方案)

Generator 可以暂停和恢复执行,配合 Promise 可以实现类似同步的异步代码,但使用起来不够直观。

function* asyncGenerator() {
  const data1 = yield fetchData();
  const data2 = yield processData(data1);
  return data2;
}

// 需要手动执行器或使用 co 库
function run(generator) {
  const it = generator();
  function next(value) {
    const result = it.next(value);
    if (result.done) return result.value;
    result.value.then(next);
  }
  next();
}

run(asyncGenerator);

4. async/await

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

// async 函数总是返回 Promise
async function getData() {
  try {
    const data1 = await fetchData();
    const data2 = await processData(data1);
    const data3 = await validateData(data2);
    const result = await saveData(data3);
    console.log('最终结果', result);
    return result;
  } catch (error) {
    console.error('出错了', error);
    throw error; // 可以继续抛出
  }
}

// 调用
getData()
  .then(result => console.log('完成', result))
  .catch(error => console.error('捕获', error));

async/await 的优势:

1)代码更清晰,像同步代码一样顺序执行

2)使用 try/catch 统一处理错误,符合直觉

3)避免了 Promise 链式调用中可能出现的混乱

★ 注意事项:

// ❌ 错误用法:没有等待结果
async function badExample() {
  fetchData(); // 没有 await,不会等待
  console.log('这行会先执行');
}

// ✅ 正确用法
async function goodExample() {
  const data = await fetchData();
  console.log(data);
}

// ✅ 并发执行:用 Promise.all 优化
async function concurrentExample() {
  // 同时发起,不用等待
  const promise1 = fetchData1();
  const promise2 = fetchData2();
  
  // 等待全部完成
  const [data1, data2] = await Promise.all([promise1, promise2]);
  console.log(data1, data2);
}

三、事件循环(Event Loop)

理解异步编程,必须理解 JavaScript 的事件循环机制。

1. 核心概念

JavaScript 运行时包含:

1)调用栈:同步代码执行的地方

2)任务队列:存放异步任务回调的地方

3)事件循环:不断检查调用栈是否为空,为空则从队列中取任务执行

2. 宏任务与微任务

异步任务分为两种:

1)宏任务(MacroTask)

setTimeout、setInterval

I/O 操作

UI 渲染

setImmediate(Node.js)

2)微任务(MicroTask)

Promise 的 then/catch/finally

async/await 中 await 之后的代码

MutationObserver

queueMicrotask

3)执行顺序:

① 执行同步代码

② 清空所有微任务

③ 执行一个宏任务

④ 清空所有微任务

重复 3-4

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

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

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

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

// 输出顺序:1, 4, 3, 2
// 更复杂的例子
async function async1() {
  console.log('async1 start');    // 同步
  await async2();                  // await 后面的代码会变成微任务
  console.log('async1 end');       // 微任务
}

async function async2() {
  console.log('async2');           // 同步
}

console.log('script start');       // 同步

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

async1();

new Promise((resolve) => {
  console.log('promise1');         // 同步
  resolve();
}).then(() => {
  console.log('promise2');         // 微任务
});

console.log('script end');         // 同步

// 输出顺序:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout

四、实际应用场景

1. 网络请求

// 使用 fetch API
async function getUserInfo(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) throw new Error('请求失败');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('获取用户信息失败', error);
    throw error;
  }
}

2. 并发控制

// 限制并发数量的函数
async function limitConcurrency(tasks, limit) {
  const results = [];
  const executing = [];
  
  for (const task of tasks) {
    const p = Promise.resolve().then(() => task());
    results.push(p);
    
    if (limit <= tasks.length) {
      const e = p.then(() => executing.splice(executing.indexOf(e), 1));
      executing.push(e);
      if (executing.length >= limit) {
        await Promise.race(executing);
      }
    }
  }
  
  return Promise.all(results);
}

3. 轮询

async function poll(fn, interval, maxAttempts) {
  let attempts = 0;
  
  while (attempts < maxAttempts) {
    try {
      const result = await fn();
      if (result) return result;
    } catch (error) {
      console.log(`第 ${attempts + 1} 次尝试失败`);
    }
    
    await new Promise(resolve => setTimeout(resolve, interval));
    attempts++;
  }
  
  throw new Error('轮询超时');
}

// 使用
const data = await poll(
  () => fetchData(),
  1000,  // 间隔1秒
  5      // 最多尝试5次
);

五、常见陷阱与最佳实践

1. 忘记 await

// ❌ 错误:没有 await,函数立即返回 Promise 对象
async function getData() {
  fetchData(); // 忘记 await
  console.log('这行会先执行');
}

// ✅ 正确
async function getData() {
  const data = await fetchData();
  console.log(data);
}

2. 循环中的 await

// ❌ 串行执行,效率低
async function processItems(items) {
  for (const item of items) {
    await processItem(item); // 一个一个处理
  }
}

// ✅ 并行执行
async function processItems(items) {
  const promises = items.map(item => processItem(item));
  await Promise.all(promises);
}

3. 错误处理

// ❌ 没有错误处理
async function riskyOperation() {
  const data = await fetchData(); // 如果失败会抛出未捕获的异常
  return data;
}

// ✅ 使用 try/catch
async function safeOperation() {
  try {
    const data = await fetchData();
    return data;
  } catch (error) {
    console.error('操作失败', error);
    return null; // 返回默认值
  }
}

4. 避免 Promise 构造器滥用

// ❌ 不必要的 Promise 包装
function bad() {
  return new Promise((resolve, reject) => {
    fetchData()
      .then(resolve)
      .catch(reject);
  });
}

// ✅ 直接返回 Promise
function good() {
  return fetchData();
}

六、总结

JavaScript 异步编程经历了从回调函数到 Promise,再到 async/await 的演进,越来越符合人类的思维方式。

核心要点:

1)异步是单线程 JavaScript 的必然选择

2)理解事件循环是掌握异步的关键

3)优先使用 async/await 编写异步代码

4)注意微任务和宏任务的执行顺序

5)合理处理错误和并发

掌握了这些内容,我们就能够应对绝大部分 JavaScript 异步编程的场景了。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐