Skip to content
Go back

JavaScript中的异步编程详解

Updated:
Edit page

什么是异步编程?

在JavaScript中,异步编程是处理可能耗时较长操作的一种方式,避免阻塞主线程。JavaScript作为单线程语言,若没有异步机制,执行长时间任务时会导致整个应用响应变慢或无响应。

Web应用中的常见异步操作包括:

异步编程的发展历程

JavaScript异步编程方案经历了多个阶段的发展,每种方式都有其适用场景。

1. 回调函数(Callbacks)

最早的异步处理方式,通过将函数作为参数传递:

function fetchData(callback) {
  setTimeout(() => {
    const data = { name: "JavaScript异步编程" };
    callback(null, data); // 第一个参数通常用于错误处理
  }, 1000);
}

fetchData((error, data) => {
  if (error) {
    console.error('获取数据失败:', error);
    return;
  }
  console.log('获取数据成功:', data.name);
});

回调函数的问题:

回调地狱示例:

fetchUserData(function(userData) {
  fetchUserPosts(userData.id, function(posts) {
    fetchPostComments(posts[0].id, function(comments) {
      fetchCommentAuthor(comments[0].authorId, function(author) {
        console.log(author.name);
        // 更多嵌套...
      });
    });
  });
});

2. Promise

Promise是ES6引入的异步编程解决方案,用于表示一个异步操作的最终完成(或失败)及其结果值:

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const success = true;
      if (success) {
        resolve({ name: "JavaScript异步编程" });
      } else {
        reject(new Error("数据获取失败"));
      }
    }, 1000);
  });
}

fetchData()
  .then(data => {
    console.log('获取数据成功:', data.name);
    return processData(data);
  })
  .then(processedData => {
    console.log('数据处理完成:', processedData);
  })
  .catch(error => {
    console.error('操作失败:', error);
  })
  .finally(() => {
    console.log('操作结束,无论成功或失败');
  });

Promise的优势:

Promise的常用方法:

Promise.all()

Promise.all() 接收一个Promise数组作为输入,并返回一个新的Promise。当所有输入的Promise都成功解决时,返回的Promise才会成功,并得到一个包含所有结果的数组;如果任何一个输入的Promise被拒绝,返回的Promise就会立即被拒绝,拒绝原因是第一个被拒绝的Promise的拒绝原因。

// 同时请求多个API端点
const promises = [
  fetch('/api/users').then(res => res.json()),
  fetch('/api/products').then(res => res.json()),
  fetch('/api/orders').then(res => res.json())
];

Promise.all(promises)
  .then(([users, products, orders]) => {
    console.log('所有数据获取成功:');
    console.log('用户数量:', users.length);
    console.log('产品数量:', products.length);
    console.log('订单数量:', orders.length);
  })
  .catch(error => {
    console.error('至少一个请求失败:', error);
    // 注意:如果有任何一个请求失败,就会进入这里
    // 且不会返回任何成功请求的结果
  });

适用场景

Promise.race()

Promise.race() 同样接收一个Promise数组,但返回的新Promise会”竞争”完成 - 一旦数组中的任何一个Promise完成(无论成功或失败),返回的Promise就会立即采用那个Promise的状态和结果。

// 模拟数据请求,带有不同的响应时间
function fetchFromSource1() {
  return new Promise((resolve) => {
    setTimeout(() => resolve('数据源1的结果'), 3000);
  });
}

function fetchFromSource2() {
  return new Promise((resolve) => {
    setTimeout(() => resolve('数据源2的结果'), 1000); // 更快
  });
}

function fetchWithTimeout(timeoutMs) {
  return new Promise((_, reject) => {
    setTimeout(() => reject(new Error('请求超时')), timeoutMs);
  });
}

// 任何一个先完成就采用其结果
Promise.race([
  fetchFromSource1(),
  fetchFromSource2(),
  fetchWithTimeout(2000) // 2秒超时
])
  .then(result => {
    console.log('最快的结果是:', result); // 数据源2的结果
  })
  .catch(error => {
    console.error('发生错误或超时:', error);
  });

适用场景

Promise.allSettled()

ES2020引入的Promise.allSettled()方法接收一个Promise数组,但与Promise.all()不同,它会等待所有Promise都完成(无论成功还是失败),然后返回一个包含每个Promise结果的对象数组,每个对象包含状态(fulfilled或rejected)和值(或错误)。

const promises = [
  fetch('/api/users').then(res => res.json()),
  fetch('/api/nonexistent').then(res => res.json()), // 这个会失败
  fetch('/api/products').then(res => res.json())
];

Promise.allSettled(promises)
  .then(results => {
    console.log('所有请求已完成,结果如下:');
    results.forEach((result, index) => {
      if (result.status === 'fulfilled') {
        console.log(`请求${index+1}成功:`, result.value);
      } else {
        console.log(`请求${index+1}失败:`, result.reason);
      }
    });
    
    // 提取所有成功的结果
    const successfulResults = results
      .filter(result => result.status === 'fulfilled')
      .map(result => result.value);
    
    console.log('所有成功的结果:', successfulResults);
  });

适用场景

Promise.any()

ES2021引入的Promise.any()接收一个Promise数组,并返回一个在任意一个输入Promise成功时立即成功的新Promise,结果就是第一个成功的Promise的值。只有所有Promise都被拒绝时,返回的Promise才会被拒绝,拒绝原因是一个AggregateError,包含所有拒绝原因。

// 模拟多个API端点,有些可能会失败
function fetchFromAPI1() {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('API1失败')), 1000);
  });
}

function fetchFromAPI2() {
  return new Promise((resolve) => {
    setTimeout(() => resolve('API2成功的数据'), 2000);
  });
}

function fetchFromAPI3() {
  return new Promise((resolve) => {
    setTimeout(() => resolve('API3成功的数据'), 3000);
  });
}

Promise.any([fetchFromAPI1(), fetchFromAPI2(), fetchFromAPI3()])
  .then(firstSuccess => {
    console.log('第一个成功的结果是:', firstSuccess); // API2成功的数据
  })
  .catch(error => {
    // 只有所有Promise都失败时才会执行这里
    console.error('所有请求均失败:', error);
    // error是AggregateError类型,包含所有拒绝原因
    error.errors.forEach((err, i) => {
      console.error(`API${i+1}错误:`, err);
    });
  });

适用场景

这些Promise方法的比较

方法成功条件失败条件返回值ES版本
Promise.all()所有Promise均成功任一Promise失败所有结果的数组ES6
Promise.race()第一个完成的Promise成功第一个完成的Promise失败第一个完成的Promise的值ES6
Promise.allSettled()总是成功不会失败所有Promise状态和结果的对象数组ES2020
Promise.any()任一Promise成功所有Promise均失败第一个成功的Promise的值ES2021

3. async/await

ES8(ES2017)引入的语法糖,基于Promise,让异步代码看起来更像同步代码:

async function getData() {
  try {
    const data = await fetchData(); // fetchData返回Promise
    console.log('获取数据成功:', data.name);
    
    const processedData = await processData(data);
    console.log('数据处理完成:', processedData);
    
    return processedData;
  } catch (error) {
    console.error('操作失败:', error);
  } finally {
    console.log('操作结束,无论成功或失败');
  }
}

// 调用异步函数
getData().then(result => {
  console.log('最终结果:', result);
});

async/await优势:

实际应用场景

API请求示例

使用Fetch API结合async/await:

async function fetchUserData(userId) {
  try {
    const response = await fetch(`https://api.example.com/users/${userId}`);
    if (!response.ok) {
      throw new Error(`HTTP错误! 状态码: ${response.status}`);
    }
    const userData = await response.json();
    return userData;
  } catch (error) {
    console.error('获取用户数据失败:', error);
    throw error;
  }
}

// 使用方法
try {
  const user = await fetchUserData(123);
  console.log(user);
} catch (error) {
  showErrorToUser(error.message);
}

并行处理多个请求

async function fetchMultipleResources() {
  try {
    // 同时发起多个请求
    const [users, products, orders] = await Promise.all([
      fetch('/api/users').then(r => r.json()),
      fetch('/api/products').then(r => r.json()),
      fetch('/api/orders').then(r => r.json())
    ]);
    
    return { users, products, orders };
  } catch (error) {
    console.error('获取数据失败:', error);
    throw error;
  }
}

异步编程最佳实践

  1. 始终处理错误:无论使用哪种异步方式,都要妥善处理可能的错误
  2. 避免过度嵌套:使用Promise链或async/await而非嵌套回调
  3. 适当使用并行处理:对于互不依赖的多个异步操作,使用Promise.all并行处理

相关资源


Edit page
Share this post on:

Previous Post
JavaScript类型转换和严格模式
Next Post
Elysia.js学习笔记