13、异步 JavaScript:Promise、Async/Await 深入解析

异步 JavaScript:Promise、Async/Await 深入解析

在 JavaScript 中,异步编程是一个核心概念,它允许我们在不阻塞主线程的情况下处理耗时操作。本文将深入探讨 Promise 链、Promise 静态方法以及 Async/Await 语法,帮助你更好地理解和运用异步编程。

1. Promise 链与错误处理

Promise 链是处理异步操作序列的一种方式。当一个 Promise 被解决或拒绝时,会触发链中的下一个 then 或 catch 方法。

// 示例代码
fetch(url1)
  .then(parseFetchResponse)
  .then((data1) => {
    console.log(data1);
    return fetch(url2);
  })
  .then(parseFetchResponse)
  .then((data2) => {
    console.log(data2);
    return fetch(url3);
  })
  .then(parseFetchResponse)
  .then((data3) => {
    console.log(data3);
  })
  .catch((error) => {
    console.log(error.message);
  });

function parseFetchResponse(response) {
  if (response.ok) {
    return response.json();
  } else {
    throw new Error("request failed");
  }
}

在这个示例中,我们依次发起三个 HTTP 请求,每个请求完成后解析响应数据。如果任何一个请求失败,会触发 catch 方法处理错误。

错误处理:then 与 catch 的区别
在 then 方法中可以通过传递第二个参数来注册拒绝处理程序,但要注意,这个拒绝处理程序只有在原始 Promise 被拒绝时才会被调用。如果 then 方法返回的 Promise 被拒绝,该拒绝处理程序不会被调用。

fakeRequest().then(
  (response) => {
    throw new Error("error");
  },
  (error) => {
    console.log(error.message); // 不会被调用
  }
);

为了避免未处理的 Promise 拒绝,建议始终使用 catch 方法来处理可能的错误。

2. Promise 静态方法的常见用例

Promise 有几个静态方法,其中 Promise.all 和 Promise.race 是最常用的。

2.1 并发请求:Promise.all
当我们需要同时发起多个独立的 HTTP 请求并等待所有请求完成时,可以使用 Promise.all 方法。

const url1 = "https://jsonplaceholder.typicode.com/todos/1";
const url2 = "https://jsonplaceholder.typicode.com/todos/2";
const url3 = "https://jsonplaceholder.typicode.com/todos/3";

Promise.all([
  fetch(url1).then(parseFetchResponse),
  fetch(url2).then(parseFetchResponse),
  fetch(url3).then(parseFetchResponse)
])
  .then((dataArr) => {
    console.log(dataArr);
  })
  .catch((error) => console.log(error.message));

Promise.all 接受一个可迭代对象(如数组)作为输入,返回一个新的 Promise。当所有输入的 Promise 都被解决时,返回的 Promise 才会被解决,其解决值是一个包含所有输入 Promise 解决值的数组。如果任何一个输入的 Promise 被拒绝,返回的 Promise 会立即被拒绝。

2.2 请求超时:Promise.race
在某些情况下,我们不希望一个 HTTP 请求长时间处于挂起状态。可以使用 Promise.race 方法实现请求超时功能。

function delayedRequest() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("hello world");
    }, 8000);
  });
}

function timeout() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const error = new Error("request timed out");
      reject(error);
    }, 3000);
  });
}

Promise.race([delayedRequest(), timeout()])
  .then((response) => {
    console.log(response);
  })
  .catch((error) => console.log(error.message));

Promise.race 同样接受一个可迭代对象作为输入,返回一个新的 Promise。当输入的 Promise 中有一个被解决或拒绝时,返回的 Promise 会立即以相同的状态被解决或拒绝。

3. Async/Await 语法

虽然 Promise 解决了回调地狱和错误处理的问题,但使用回调函数来注册解决和拒绝处理程序仍然显得冗长。Async/Await 语法是一种更简洁、直观的处理 Promise 的方式。

3.1 基本用法
使用 Async/Await 语法需要遵循两个主要步骤:
1. 使用 async 关键字标记函数,因为 await 关键字只能在 async 函数中使用。
2. 在 async 函数内部使用 await 关键字等待 Promise 解决。

async function fetchTodo(url) {
  try {
    const response = await fetch(url);
    if (response.ok) {
      const data = await response.json();
      console.log(data);
    } else {
      throw new Error("request failed");
    }
  } catch (error) {
    console.log(error.message);
  }
}

const url = "https://jsonplaceholder.typicode.com/todos/1";
fetchTodo(url);

在这个示例中,我们使用 Async/Await 重写了之前的 fetchTodo 函数。代码看起来更像同步代码,但实际上是异步执行的。

3.2 async 函数
async 函数总是返回一个 Promise。函数内部的返回值会被包装在一个已解决的 Promise 中,而抛出的错误会导致 Promise 被拒绝。

async function foo() {
  return 123;
}

foo().then(console.log); // 123

async function bar() {
  throw new Error("some error occurred");
}

bar().catch((error) => console.log(error.message)); // some error occurred

3.3 await 关键字
await 关键字用于等待 Promise 解决。它会暂停 async 函数的执行,直到 Promise 被解决或拒绝。

async function example() {
  const response = await fetch(url);
  const data = await response.json();
  console.log(data);
}

在这个示例中,await 表达式会直接计算 Promise 的解决值,我们可以将其保存到变量中。与 Promise 链不同,不需要显式注册解决处理程序。

4. 多个 await 表达式

在 async 函数中可以使用多个 await 表达式,但要注意它们是按顺序执行的,不会并行执行。

function promisifiedRandomNumber() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const randomNum = Math.floor(Math.random() * 10);
      resolve(randomNum);
    }, 1000);
  });
}

async function random() {
  const num1 = await promisifiedRandomNumber();
  const num2 = await promisifiedRandomNumber();
  const num3 = await promisifiedRandomNumber();
  console.log(num1, num2, num3);
}

random();

在这个示例中,每个 await 表达式都会等待大约 1 秒,整个函数执行大约需要 3 秒。

如果需要并发执行多个异步操作,可以使用 Promise.all 方法。

async function random() {
  const promiseArr = [
    promisifiedRandomNumber(),
    promisifiedRandomNumber(),
    promisifiedRandomNumber()
  ];
  const randomNumsArr = await Promise.all(promiseArr);
  console.log(randomNumsArr);
}

random();
5. 错误处理

在 async 函数中,可以使用 try-catch 块来处理 Promise 拒绝。

async function getUsersAndTasks() {
  try {
    const users = await fetchUsers();
    const tasks = await fetchTasks();
  } catch (error) {
    // 处理错误
  }
}

通过这种方式,我们可以捕获并处理任何异步操作中抛出的错误。

总结

本文介绍了 JavaScript 中异步编程的重要概念,包括 Promise 链、Promise 静态方法和 Async/Await 语法。掌握这些知识可以帮助我们编写更简洁、可维护的异步代码。以下是一些关键点总结:
- 使用 Promise 链按顺序处理多个异步操作,并使用 catch 方法处理错误。
- 使用 Promise.all 并发执行多个独立的异步操作。
- 使用 Promise.race 实现请求超时功能。
- 使用 Async/Await 语法以更简洁、直观的方式处理 Promise。
- 在 async 函数中使用 try-catch 块处理可能的错误。

希望这些内容对你理解和运用 JavaScript 异步编程有所帮助。

异步 JavaScript:Promise、Async/Await 深入解析

6. 异步编程的实际应用场景

异步编程在实际开发中有广泛的应用场景,下面将介绍几个常见的场景及如何运用上述知识解决问题。

6.1 数据加载与渲染
在前端开发中,经常需要从服务器获取数据并渲染到页面上。使用异步编程可以避免页面卡顿,提高用户体验。

async function loadDataAndRender() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    // 渲染数据到页面
    renderData(data);
  } catch (error) {
    console.log('数据加载失败:', error.message);
  }
}

function renderData(data) {
  // 实现数据渲染逻辑
  const container = document.getElementById('data-container');
  container.innerHTML = JSON.stringify(data);
}

loadDataAndRender();

在这个示例中,我们使用 Async/Await 从服务器获取数据,然后调用 renderData 函数将数据渲染到页面上。如果数据加载失败,会在控制台输出错误信息。

6.2 多资源加载
当需要同时加载多个资源(如图片、脚本等)时,可以使用 Promise.all 并发加载。

function loadImage(url) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.onerror = () => reject(new Error('图片加载失败'));
    img.src = url;
  });
}

async function loadMultipleImages() {
  const imageUrls = [
    'https://example.com/image1.jpg',
    'https://example.com/image2.jpg',
    'https://example.com/image3.jpg'
  ];
  const promises = imageUrls.map(url => loadImage(url));
  try {
    const images = await Promise.all(promises);
    // 所有图片加载完成后处理
    images.forEach(img => document.body.appendChild(img));
  } catch (error) {
    console.log('图片加载出错:', error.message);
  }
}

loadMultipleImages();

在这个示例中,我们定义了 loadImage 函数用于加载单个图片,返回一个 Promise。然后使用 Promise.all 并发加载多个图片,当所有图片加载完成后,将它们添加到页面上。

7. 异步编程的性能优化

在异步编程中,合理的性能优化可以提高程序的响应速度和资源利用率。

7.1 避免不必要的等待
在使用多个 await 表达式时,要确保它们是必要的顺序执行。如果某些操作可以并发执行,应使用 Promise.all Promise.race

// 不好的示例
async function badExample() {
  const result1 = await someAsyncOperation1();
  const result2 = await someAsyncOperation2();
  return [result1, result2];
}

// 好的示例
async function goodExample() {
  const promise1 = someAsyncOperation1();
  const promise2 = someAsyncOperation2();
  const [result1, result2] = await Promise.all([promise1, promise2]);
  return [result1, result2];
}

badExample 中, someAsyncOperation2 必须等待 someAsyncOperation1 完成后才能开始执行。而在 goodExample 中,两个操作并发执行,提高了性能。

7.2 控制并发数量
当需要处理大量异步任务时,可能会导致资源耗尽。可以使用队列来控制并发数量。

function asyncTask(task) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(task);
    }, Math.random() * 1000);
  });
}

async function limitConcurrency(tasks, limit) {
  const queue = [...tasks];
  const running = [];
  const results = [];

  while (queue.length > 0 || running.length > 0) {
    while (running.length < limit && queue.length > 0) {
      const task = queue.shift();
      const promise = asyncTask(task).then(result => {
        results.push(result);
        running.splice(running.indexOf(promise), 1);
      });
      running.push(promise);
    }
    await Promise.race(running);
  }

  return results;
}

const tasks = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
limitConcurrency(tasks, 3).then(results => {
  console.log('任务完成:', results);
});

在这个示例中,我们定义了 limitConcurrency 函数,它接受一个任务数组和并发限制数量作为参数。通过队列和 Promise.race 控制并发数量,确保同时执行的任务不超过指定数量。

8. 异步编程的流程图

为了更好地理解异步编程的执行流程,下面是一个简单的 Async/Await 执行流程图:

graph TD;
    A[调用 async 函数] --> B[同步执行代码];
    B --> C{是否遇到 await};
    C -- 是 --> D[暂停函数执行];
    D --> E[等待 Promise 解决];
    E -- 解决 --> F[恢复函数执行];
    F --> G[继续执行后续代码];
    C -- 否 --> G;
    G --> H{是否还有 await};
    H -- 是 --> C;
    H -- 否 --> I[函数执行结束];

这个流程图展示了 Async/Await 函数的执行过程。当遇到 await 表达式时,函数执行会暂停,等待 Promise 解决后再继续执行。

9. 总结与展望

异步编程是 JavaScript 中非常重要的一部分,它允许我们处理耗时操作而不阻塞主线程。通过 Promise 链、Promise 静态方法和 Async/Await 语法,我们可以编写更简洁、可维护的异步代码。

在未来的开发中,异步编程的应用场景会越来越广泛,同时也会有更多的工具和技术出现来简化异步编程。例如,ES2022 引入了顶级 await,允许在模块的顶层使用 await 关键字,进一步简化了异步代码的编写。

希望本文能帮助你深入理解 JavaScript 异步编程,并在实际项目中灵活运用这些知识。不断学习和实践,你将能够更好地掌握异步编程,提高代码的质量和性能。

总结

本文全面介绍了 JavaScript 异步编程的核心知识,从 Promise 链、Promise 静态方法到 Async/Await 语法,再到实际应用场景和性能优化。以下是关键知识点总结:
1. Promise 链 :用于按顺序处理多个异步操作,使用 catch 方法处理错误。
2. Promise 静态方法
- Promise.all :并发执行多个独立的异步操作。
- Promise.race :实现请求超时功能。
3. Async/Await 语法 :以更简洁、直观的方式处理 Promise,使用 try-catch 块处理错误。
4. 实际应用场景 :数据加载与渲染、多资源加载等。
5. 性能优化 :避免不必要的等待,控制并发数量。

掌握这些知识,你可以编写高效、可维护的异步代码,提升 JavaScript 编程能力。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值