Skip to content

JavaScript异步编程

JavaScript是一种单线程语言,但它能够处理异步操作,这使得它在处理网络请求、文件操作和定时任务等方面非常强大。本文将深入探讨JavaScript中的异步编程模式和技术。

异步编程基础

同步与异步

在理解异步编程之前,我们需要先了解同步和异步的区别:

  • 同步执行:代码按照顺序一步一步执行,每一步都必须等待前一步完成。
  • 异步执行:代码不必等待耗时操作完成,而是继续执行后续代码,当耗时操作完成时,通过回调、Promise等机制处理结果。
javascript
// 同步执行示例
console.log("步骤 1");
console.log("步骤 2");
console.log("步骤 3");
// 输出顺序:步骤 1, 步骤 2, 步骤 3

// 异步执行示例
console.log("步骤 1");
setTimeout(() => {
  console.log("步骤 2");
}, 1000);
console.log("步骤 3");
// 输出顺序:步骤 1, 步骤 3, 步骤 2

JavaScript的事件循环

JavaScript的异步操作依赖于事件循环(Event Loop)机制:

  1. 调用栈(Call Stack):执行代码的地方,函数调用形成一个栈结构。
  2. 任务队列(Task Queue):存放异步任务的回调函数。
  3. 微任务队列(Microtask Queue):存放优先级更高的异步任务(如Promise回调)。
  4. 事件循环:不断检查调用栈是否为空,如果为空则依次执行微任务队列和任务队列中的任务。
javascript
console.log("开始");

setTimeout(() => {
  console.log("定时器回调");
}, 0);

Promise.resolve().then(() => {
  console.log("Promise回调");
});

console.log("结束");

// 输出顺序:开始, 结束, Promise回调, 定时器回调

回调函数

回调函数是最早用于处理异步操作的方式,它是一个作为参数传递给另一个函数的函数,在适当的时候被调用。

基本用法

javascript
function fetchData(callback) {
  // 模拟网络请求
  setTimeout(() => {
    const data = { name: "张三", age: 30 };
    callback(data);
  }, 1000);
}

fetchData((data) => {
  console.log("获取到的数据:", data);
});

console.log("请求已发送");

// 输出顺序:请求已发送, 获取到的数据: { name: '张三', age: 30 }

回调地狱

当多个异步操作需要依次执行时,回调函数会导致代码嵌套过深,形成"回调地狱":

javascript
fetchUserData(userId, (user) => {
  fetchUserPosts(user.id, (posts) => {
    fetchPostComments(posts[0].id, (comments) => {
      fetchCommentAuthor(comments[0].authorId, (author) => {
        console.log("作者信息:", author);
        // 更多嵌套...
      });
    });
  });
});

错误处理

回调函数的错误处理通常通过传递错误参数实现:

javascript
function fetchData(callback) {
  // 模拟网络请求
  setTimeout(() => {
    const success = Math.random() > 0.5;
    if (success) {
      const data = { name: "张三", age: 30 };
      callback(null, data);
    } else {
      callback(new Error("获取数据失败"), null);
    }
  }, 1000);
}

fetchData((error, data) => {
  if (error) {
    console.error("错误:", error.message);
    return;
  }
  console.log("获取到的数据:", data);
});

Promise

Promise是ES6引入的一种处理异步操作的对象,它代表一个异步操作的最终完成(或失败)及其结果值。

基本用法

javascript
function fetchData() {
  return new Promise((resolve, reject) => {
    // 模拟网络请求
    setTimeout(() => {
      const success = Math.random() > 0.5;
      if (success) {
        const data = { name: "张三", age: 30 };
        resolve(data);
      } else {
        reject(new Error("获取数据失败"));
      }
    }, 1000);
  });
}

fetchData()
  .then(data => {
    console.log("获取到的数据:", data);
  })
  .catch(error => {
    console.error("错误:", error.message);
  })
  .finally(() => {
    console.log("请求结束");
  });

Promise链式调用

Promise可以链式调用,解决回调地狱问题:

javascript
fetchUserData(userId)
  .then(user => {
    return fetchUserPosts(user.id);
  })
  .then(posts => {
    return fetchPostComments(posts[0].id);
  })
  .then(comments => {
    return fetchCommentAuthor(comments[0].authorId);
  })
  .then(author => {
    console.log("作者信息:", author);
  })
  .catch(error => {
    console.error("错误:", error.message);
  });

Promise.all

Promise.all接收一个Promise数组,当所有Promise都成功时返回所有结果的数组,如果有任何一个Promise失败,则立即返回失败:

javascript
const promise1 = fetchUserData(1);
const promise2 = fetchUserData(2);
const promise3 = fetchUserData(3);

Promise.all([promise1, promise2, promise3])
  .then(results => {
    const [user1, user2, user3] = results;
    console.log("所有用户数据:", user1, user2, user3);
  })
  .catch(error => {
    console.error("获取用户数据失败:", error.message);
  });

Promise.race

Promise.race接收一个Promise数组,返回最先完成的Promise的结果,无论成功或失败:

javascript
const promise1 = fetchData("endpoint1"); // 可能需要2秒
const promise2 = fetchData("endpoint2"); // 可能需要1秒

Promise.race([promise1, promise2])
  .then(result => {
    console.log("最快返回的数据:", result);
  })
  .catch(error => {
    console.error("最快返回的错误:", error.message);
  });

Promise.allSettled

Promise.allSettled(ES2020引入)接收一个Promise数组,等待所有Promise完成(无论成功或失败),并返回它们的结果:

javascript
const promises = [
  fetchUserData(1),
  fetchUserData(2),
  fetchUserData(999) // 假设这个ID不存在
];

Promise.allSettled(promises)
  .then(results => {
    results.forEach((result, index) => {
      if (result.status === "fulfilled") {
        console.log(`用户${index + 1}数据:`, result.value);
      } else {
        console.log(`用户${index + 1}数据获取失败:`, result.reason);
      }
    });
  });

Promise.any

Promise.any(ES2021引入)接收一个Promise数组,返回第一个成功的Promise的结果,如果所有Promise都失败,则返回一个包含所有失败原因的AggregateError:

javascript
const promises = [
  fetchData("endpoint1"),
  fetchData("endpoint2"),
  fetchData("endpoint3")
];

Promise.any(promises)
  .then(result => {
    console.log("第一个成功的结果:", result);
  })
  .catch(error => {
    console.log("所有Promise都失败了");
    console.log("失败原因:", error.errors);
  });

async/await

async/await是ES2017引入的语法糖,基于Promise,使异步代码看起来更像同步代码,提高了可读性。

基本用法

javascript
async function fetchUserData(userId) {
  try {
    const user = await fetchUser(userId);
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts[0].id);
    const author = await fetchAuthor(comments[0].authorId);
    
    console.log("作者信息:", author);
    return author;
  } catch (error) {
    console.error("获取数据失败:", error.message);
    throw error;
  }
}

// 调用异步函数
fetchUserData(1)
  .then(author => {
    console.log("处理作者数据");
  })
  .catch(error => {
    console.error("外部错误处理:", error.message);
  });

并行执行

使用Promise.all配合await可以并行执行多个异步操作:

javascript
async function fetchAllData() {
  try {
    const [users, posts, comments] = await Promise.all([
      fetchUsers(),
      fetchPosts(),
      fetchComments()
    ]);
    
    console.log("用户数:", users.length);
    console.log("文章数:", posts.length);
    console.log("评论数:", comments.length);
    
    return { users, posts, comments };
  } catch (error) {
    console.error("获取数据失败:", error.message);
    throw error;
  }
}

循环中的异步操作

在循环中使用async/await需要注意是串行执行还是并行执行:

javascript
// 串行执行(一个接一个)
async function fetchUsersSequentially(userIds) {
  const users = [];
  
  for (const id of userIds) {
    const user = await fetchUser(id); // 等待每个请求完成
    users.push(user);
  }
  
  return users;
}

// 并行执行(同时发起所有请求)
async function fetchUsersParallel(userIds) {
  const promises = userIds.map(id => fetchUser(id));
  const users = await Promise.all(promises);
  return users;
}

错误处理

async/await中的错误处理可以使用try/catch:

javascript
async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    
    if (!response.ok) {
      throw new Error(`HTTP错误: ${response.status}`);
    }
    
    const data = await response.json();
    return data;
  } catch (error) {
    console.error("获取数据失败:", error.message);
    // 可以选择重新抛出错误或返回默认值
    return { error: true, message: error.message };
  }
}

生成器函数与异步迭代

生成器函数(Generator Functions)可以暂停和恢复执行,使其成为处理异步操作的另一种方式。

基本用法

javascript
function* simpleGenerator() {
  console.log("开始执行");
  yield 1;
  console.log("继续执行");
  yield 2;
  console.log("最后执行");
  return 3;
}

const gen = simpleGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: true }

使用生成器处理异步操作

javascript
function fetchData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`来自 ${url} 的数据`);
    }, 1000);
  });
}

function* fetchMultipleData() {
  try {
    const data1 = yield fetchData('url1');
    console.log(data1);
    
    const data2 = yield fetchData('url2');
    console.log(data2);
    
    return "所有数据获取完成";
  } catch (error) {
    console.error("错误:", error);
  }
}

// 手动执行生成器
function runGenerator(gen) {
  const iterator = gen();
  
  function run(arg) {
    const result = iterator.next(arg);
    
    if (result.done) {
      return result.value;
    }
    
    return Promise.resolve(result.value).then(
      value => run(value),
      error => {
        iterator.throw(error);
        return run();
      }
    );
  }
  
  return run();
}

runGenerator(fetchMultipleData)
  .then(result => console.log(result))
  .catch(error => console.error(error));

异步迭代器

ES2018引入了异步迭代器和for await...of语法,简化了对异步数据源的迭代:

javascript
async function* asyncGenerator() {
  yield await fetchData('url1');
  yield await fetchData('url2');
  yield await fetchData('url3');
}

async function processData() {
  for await (const data of asyncGenerator()) {
    console.log("处理数据:", data);
  }
}

processData();

实际应用示例

使用Fetch API获取数据

javascript
// 使用Promise
function fetchUserData(userId) {
  return fetch(`https://api.example.com/users/${userId}`)
    .then(response => {
      if (!response.ok) {
        throw new Error(`HTTP错误: ${response.status}`);
      }
      return response.json();
    });
}

// 使用async/await
async function fetchUserData(userId) {
  const response = await fetch(`https://api.example.com/users/${userId}`);
  
  if (!response.ok) {
    throw new Error(`HTTP错误: ${response.status}`);
  }
  
  return response.json();
}

实现超时处理

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

// 使用
fetchWithTimeout('https://api.example.com/data', 3000)
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error.message));

重试机制

javascript
async function fetchWithRetry(url, retries = 3, delay = 1000) {
  let lastError;
  
  for (let i = 0; i < retries; i++) {
    try {
      return await fetch(url);
    } catch (error) {
      console.log(`尝试 ${i + 1} 失败,${i < retries - 1 ? '重试中...' : '放弃'}`);
      lastError = error;
      
      if (i < retries - 1) {
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }
  
  throw lastError;
}

并发控制

javascript
async function fetchWithConcurrencyLimit(urls, limit = 3) {
  const results = [];
  const inProgress = new Set();
  
  async function doFetch(url) {
    try {
      const response = await fetch(url);
      const data = await response.json();
      return data;
    } finally {
      inProgress.delete(url);
    }
  }
  
  for (const url of urls) {
    // 等待,直到并发数量低于限制
    while (inProgress.size >= limit) {
      await Promise.race(inProgress);
    }
    
    // 添加到进行中的请求
    const promise = doFetch(url);
    inProgress.add(promise);
    
    // 存储结果(保持顺序)
    results.push(await promise);
  }
  
  return results;
}

异步编程最佳实践

错误处理

始终处理异步操作中的错误:

javascript
// Promise
fetchData()
  .then(data => {
    // 处理数据
  })
  .catch(error => {
    // 处理错误
    console.error("错误:", error);
  });

// async/await
async function fetchData() {
  try {
    const data = await fetchSomething();
    // 处理数据
  } catch (error) {
    // 处理错误
    console.error("错误:", error);
  }
}

避免回调地狱

使用Promise或async/await替代嵌套回调:

javascript
// 不推荐
doSomething(function(result1) {
  doSomethingElse(result1, function(result2) {
    doAnotherThing(result2, function(result3) {
      console.log(result3);
    });
  });
});

// 推荐 (Promise)
doSomething()
  .then(result1 => doSomethingElse(result1))
  .then(result2 => doAnotherThing(result2))
  .then(result3 => {
    console.log(result3);
  })
  .catch(error => {
    console.error(error);
  });

// 推荐 (async/await)
async function doSequentialWork() {
  try {
    const result1 = await doSomething();
    const result2 = await doSomethingElse(result1);
    const result3 = await doAnotherThing(result2);
    console.log(result3);
  } catch (error) {
    console.error(error);
  }
}

并行执行

当多个异步操作相互独立时,应该并行执行它们:

javascript
// 串行(慢)
async function fetchSequential() {
  const user = await fetchUser();
  const posts = await fetchPosts();
  const comments = await fetchComments();
  
  return { user, posts, comments };
}

// 并行(快)
async function fetchParallel() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchComments()
  ]);
  
  return { user, posts, comments };
}

取消异步操作

使用AbortController取消fetch请求:

javascript
function fetchWithCancel(url) {
  const controller = new AbortController();
  const signal = controller.signal;
  
  const promise = fetch(url, { signal })
    .then(response => response.json());
  
  return {
    promise,
    cancel: () => controller.abort()
  };
}

// 使用
const { promise, cancel } = fetchWithCancel('https://api.example.com/data');

// 5秒后取消请求
setTimeout(() => {
  console.log('取消请求');
  cancel();
}, 5000);

promise
  .then(data => console.log('数据:', data))
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('请求已取消');
    } else {
      console.error('错误:', error);
    }
  });

异步函数组合

创建可组合的异步函数:

javascript
// 异步函数组合
const composeAsync = (...fns) => input => 
  fns.reduce(
    (chain, fn) => chain.then(fn),
    Promise.resolve(input)
  );

// 使用
const getUserData = composeAsync(
  fetchUser,
  user => fetchPosts(user.id),
  posts => fetchComments(posts[0].id)
);

getUserData(1)
  .then(comments => console.log('评论:', comments))
  .catch(error => console.error('错误:', error));

总结

JavaScript的异步编程已经从回调函数发展到Promise,再到async/await,使得处理异步操作变得越来越简洁和直观。掌握这些异步编程技术对于开发高性能、响应式的JavaScript应用至关重要。

  • 回调函数是最基本的异步处理方式,但容易导致回调地狱。
  • Promise提供了更好的链式调用和错误处理机制。
  • async/await让异步代码看起来更像同步代码,提高了可读性。
  • 生成器和异步迭代器为处理异步数据流提供了强大工具。

在实际应用中,应根据具体需求选择合适的异步编程模式,并遵循最佳实践,如正确处理错误、避免回调地狱、并行执行独立操作等。

用知识点燃技术的火山