异步编程实现方式

JavaScript 中常见的异步编程方案及代码示例

问题

JavaScript 中有哪些异步编程的实现方式?各自的特点是什么?

解答

1. 回调函数

最基础的异步方式,将函数作为参数传递。

// 模拟异步请求
function fetchData(callback) {
  setTimeout(() => {
    callback(null, { name: 'Alice' });
  }, 1000);
}

// 使用回调
fetchData((err, data) => {
  if (err) {
    console.error(err);
    return;
  }
  console.log(data); // { name: 'Alice' }
});

// 回调地狱示例
fetchData((err, data1) => {
  fetchData((err, data2) => {
    fetchData((err, data3) => {
      // 嵌套过深,难以维护
    });
  });
});

2. Promise

解决回调地狱,支持链式调用。

// 创建 Promise
function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ name: 'Alice' });
    }, 1000);
  });
}

// 链式调用
fetchData()
  .then(data => {
    console.log(data);
    return fetchData();
  })
  .then(data => {
    console.log(data);
  })
  .catch(err => {
    console.error(err);
  });

// 并行执行
Promise.all([fetchData(), fetchData()])
  .then(([data1, data2]) => {
    console.log(data1, data2);
  });

// 竞速执行
Promise.race([fetchData(), fetchData()])
  .then(data => {
    console.log('最快返回:', data);
  });

3. Generator

通过 yield 暂停执行,需要手动或借助执行器驱动。

// Generator 函数
function* fetchGenerator() {
  const data1 = yield fetchData();
  console.log(data1);
  const data2 = yield fetchData();
  console.log(data2);
  return 'done';
}

// 简易执行器
function run(generator) {
  const gen = generator();
  
  function next(data) {
    const result = gen.next(data);
    if (result.done) return result.value;
    // 假设 yield 的值是 Promise
    result.value.then(next);
  }
  
  next();
}

run(fetchGenerator);

4. async/await

Generator 的语法糖,最直观的异步写法。

// async 函数
async function getData() {
  try {
    // await 等待 Promise 完成
    const data1 = await fetchData();
    console.log(data1);
    
    const data2 = await fetchData();
    console.log(data2);
    
    // 并行执行
    const [data3, data4] = await Promise.all([
      fetchData(),
      fetchData()
    ]);
    
    return { data1, data2, data3, data4 };
  } catch (err) {
    console.error(err);
  }
}

getData();

5. 事件监听

基于事件触发的异步模式。

// Node.js EventEmitter
const EventEmitter = require('events');
const emitter = new EventEmitter();

// 监听事件
emitter.on('data', (data) => {
  console.log('收到数据:', data);
});

// 触发事件
setTimeout(() => {
  emitter.emit('data', { name: 'Alice' });
}, 1000);

// 浏览器中的事件监听
document.addEventListener('click', (e) => {
  console.log('点击位置:', e.clientX, e.clientY);
});

6. 发布订阅模式

解耦事件的发布者和订阅者。

// 简易发布订阅
class PubSub {
  constructor() {
    this.events = {};
  }
  
  // 订阅
  subscribe(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
    
    // 返回取消订阅函数
    return () => {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    };
  }
  
  // 发布
  publish(event, data) {
    if (!this.events[event]) return;
    this.events[event].forEach(callback => callback(data));
  }
}

// 使用
const pubsub = new PubSub();

const unsubscribe = pubsub.subscribe('userLogin', (user) => {
  console.log('用户登录:', user);
});

pubsub.publish('userLogin', { name: 'Alice' });

unsubscribe(); // 取消订阅

各方式对比

方式优点缺点
回调函数简单直接回调地狱,错误处理麻烦
Promise链式调用,统一错误处理无法取消,语义不够直观
Generator可暂停执行需要执行器,使用复杂
async/await同步写法,易读易写需要 try/catch 处理错误
事件监听解耦,适合多次触发事件流难以追踪
发布订阅完全解耦过度使用导致代码难以理解

关键点

  • 回调函数是基础,但嵌套过深会形成回调地狱
  • Promise 通过链式调用解决嵌套问题,是现代异步的基石
  • async/await 是目前最推荐的写法,代码最接近同步风格
  • 事件监听和发布订阅适合一对多、多次触发的场景
  • 实际开发中常组合使用,如 async/await + Promise.all 处理并发