当前位置: 首页 > news >正文

async/await:在前端开发中的应用

目录

1. async await 

async

await

总结: 

2. 为什么用async / await 

3. 代码示例说明

3.1  Promise.all 

3.2  用 async await 改写 Promise.all

4.  async/await可以解决哪些异步问题

5.  哪些会产生回调地狱的问题

6.  为了避免回调地狱的问题,可以采用以下几种方法

6.1  使用传统回调函数方式导致的回调地狱:

6.2  使用Promise链式调用避免回调地狱


1. async await 

async

  • 定义‌:async 是一个用于声明异步函数的关键字。当一个函数被 async 修饰时,它会自动返回一个 Promise 对象。如果函数内部显式地返回了一个非 Promise 值,那么这个值会被自动包装成一个已解决的 Promise 对象。
  • 用法‌:将 async 关键字放在函数声明或函数表达式之前。
  • async function myFunction() {
      return 'Hello, world!';
    }
    myFunction().then(console.log); // 输出: Hello, world!
    

await

  • 定义‌:await 是一个用于等待 Promise 解决(resolve)或拒绝(reject)的操作符。它只能在 async 函数内部使用,并且会暂停 async 函数的执行,直到等待的 Promise 有结果。
  • 用法‌:将 await 关键字放在 Promise 调用或任何返回 Promise 的表达式之前。
  • 返回值‌:如果 Promise 被解决,await 表达式的结果就是 Promise 解决的值。如果 Promise 被拒绝,await 表达式会抛出一个异常,这个异常可以被 try...catch 语句捕获。
  • async function fetchData() {
      try {
        let response = await fetch('https://api.example.com/data');
        let data = await response.json();
        console.log(data);
      } catch (error) {
        console.error('Error:', error);
      }
    }
    fetchData();
    

总结: 

  • async、await 是 ES8(ECMAScript 2017)引入的新语法,用来简化 Promise 异步操作。使用 async和 await可以让异步代码看起来几乎和同步代码一样,从而大大简化了代码的结构和可读性。你不再需要嵌套回调函数或链接多个Promise,代码变得更加直观和线性。
  • async 用来声明一个 function 是异步的,await 用来等待一个异步方法执行完成。
  • await 只能出现在 async 函数中。
  • 下面图片:async 声明的函数,返回的结果是一个 Promise 对象。如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

  • 下图:如果 async 函数没有返回值

  • await 等待的是它右侧的一个表达式的返回值。这个表达式的计算结果是 Promise 对象或者其他值。

  • await 只能用于 Promise,如果你尝试在非 Promise 上使用 await,它会导致运行时错误。
  • async/await 不会阻塞主线程,它们是基于事件循环的,允许其他代码在异步操作等待期间继续执行。
  • 在 async 函数内部,你可以使用多个 await 表达式,它们会按顺序执行。
  • 错误处理通常通过 try...catch 语句来实现,以捕获 await 表达式可能抛出的异常。
  • async/await 使得异步代码更加清晰和易于理解,它减少了回调地狱和 Promise 链的复杂性,是现代 JavaScript 开发中处理异步操作的首选方法。

注:Promise.resolve(x) 等价于 new Promise(resolve => resolve(x)),用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。

2. 为什么用async / await 

asyncawait是JavaScript中用于处理异步操作的现代方法,它们提供了一种更简洁、更易读的方式来写异步代码,相比于传统的回调函数(callbacks)和Promise链,具有显著的优势。以下是使用asyncawait的几个主要原因:

  1. 简洁性‌:

    使用asyncawait可以让异步代码看起来几乎和同步代码一样,从而大大简化了代码的结构和可读性。你不再需要嵌套回调函数或链接多个Promise,代码变得更加直观和线性。
  2. 错误处理‌:

    使用try...catch语句可以很容易地捕获await表达式中抛出的异常,这使得错误处理更加直接和集中。相比之下,在Promise链中处理错误可能需要多个.catch()方法,或者在回调函数中手动检查错误。
  3. 调试方便‌:

    由于async函数在执行时会返回一个Promise,并且可以在任何await表达式处暂停,这使得在调试器中步进代码时更容易理解异步操作的执行流程。你可以逐步执行代码,看到每个异步操作的结果,而不需要跳过复杂的回调或Promise逻辑。
  4. 并发执行‌:

    虽然await会暂停async函数的执行,等待Promise的解决,但你可以通过不在每个异步操作后都使用await,而是将它们存储在变量中,然后在需要时一起等待它们解决(例如使用Promise.all()),来实现并发执行

3. 代码示例说明

3.1  Promise.all 

        是 JavaScript 中用于处理多个 Promise 对象的一个方法。它接受一个包含多个 Promise 的数组作为输入,并且只有当这个数组中的所有 Promise 都成功完成时,它才会返回一个新的 Promise,该 Promise 的结果是一个包含所有原 Promise 结果的数组。如果任何一个 Promise 失败了,Promise.all 返回的 Promise 会立即被拒绝,返回那个失败的 Promise 的原因。

const promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'first');
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 200, 'second');
});

const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 300, 'third');
});

Promise.all([promise1, promise2, promise3])
  .then((values) => {
    console.log(values); // ['first', 'second', 'third']
  })
  .catch((error) => {
    console.error(error);
  });

创建了三个 Promise,它们分别在 100 毫秒、200 毫秒和 300 毫秒后完成。我们使用 Promise.all 来等待这三个 Promise 全部完成,然后打印出它们的结果。由于所有的 Promise 都成功完成了,所以 Promise.all 返回的 Promise 也成功了,并且它的结果是一个包含所有原 Promise 结果的数组。

如果其中一个 Promise 失败了,比如我们修改 promise2 让它被拒绝:

const promise2 = new Promise((resolve, reject) => {
  setTimeout(reject, 200, 'Error in second promise');
});

那么 Promise.all 返回的 Promise 会立即被拒绝,并且它的拒绝原因会是那个失败的 Promise 的原因,即 'Error in second promise'。在这个情况下,then 方法不会被调用,而是会调用 catch 方法来处理错误。

3.2  用 async await 改写 Promise.all

function waitFor(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}


async functiom fetchAllPromise () {
    try {
        const first = await waitFor(100).then(() => 'first')
        const second = await waitFor(100).then(() => 'second')
        const third = await waitFor(100).then(() => 'third')

        // 这里所有的await都顺序执行了,因此会按照100ms, 200ms, 300ms的顺序等待
        // 然后打印出 'first', 'second', 'third'
        console.log([first, second, third]);

    } catch {
       console.error('An error occurred:', error);
    }   
}

fetchAllPromises();

按照上述这样写,fetchAllPromise 函数中,因为每个 await 都会顺序地等待前一个 Promise 解决后再继续执行下一个。为了并发地等待所有 Promise,我们应该先启动所有的等待任务,然后再使用 await 等待它们全部完成。这可以通过将 Promise 存储在数组中,然后使用 Promise.all 来实现:

function waitFor(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}


async function fetchAllPromisesConcurrently() {
  try {
    const promise1 = waitFor(100).then(() => 'first');
    const promise2 = waitFor(200).then(() => 'second');
    const promise3 = waitFor(300).then(() => 'third');

    // 并发等待所有Promise
    const results = await Promise.all([promise1, promise2, promise3]);

    // 打印所有结果
    console.log(results); // ['first', 'second', 'third']
  } catch (error) {
    console.error('An error occurred:', error);
  }
}

fetchAllPromisesConcurrently();

在这个例子中,fetchAllPromisesConcurrently 函数会并发地启动三个等待任务,并且使用 Promise.all 来等待它们全部完成。由于 Promise.all 是并发执行的,所以它会等待最长时间的那个 Promise(在这个例子中是 300 毫秒的 promise3)完成后,再一次性返回所有结果。这样,我们就可以更高效地处理多个异步操作了。

4.  async/await可以解决哪些异步问题

  1. 避免阻塞线程或进程‌:
    使用 async/await 可以避免在等待异步操作完成时阻塞线程或进程,使得应用程序能够同时执行多个异步操作,提高了程序的并发性和响应性。

  2. 简化代码结构‌:
    async/await 提供了一种更直观、易于理解的代码结构,使开发者能够以顺序方式编写异步代码,而不是嵌套回调函数。这大大简化了代码结构,使代码更加简洁和易于维护1。

  3. 简化错误处理和异常传播‌:
    使用 async/await 可以简化错误处理和异常传播的过程。通过结合 try...catch 语句,开发者可以方便地捕获和处理异步操作中的错误,使得异常处理更加清晰和可维护1。

  4. 减少回调地狱‌:
    async/await 可以减少回调地狱的问题。在传统的异步编程中,如果多个异步操作需要依次执行,往往会形成嵌套回调函数,导致代码结构复杂且难以维护。而 async/await 可以让异步代码看起来像同步代码一样,按照顺序执行多个异步操作,从而避免了回调地狱的问题。

  5. 提高代码的可读性和可维护性‌:
    async/await 支持了异步代码的可读性和可维护性。通过简化代码结构、减少回调地狱以及简化错误处理,async/await 使得异步代码更加清晰、易于理解和维护。

在使用 async/await 时也需要注意异步代码中的错误处理、死锁和资源管理等问题,以确保代码的正确性和性能。

5.  哪些会产生回调地狱的问题

  1. 多层嵌套的回调函数‌:
    当多个异步操作需要按顺序执行时,每个操作的回调函数可能会嵌套在另一个回调函数中,形成多层嵌套的结构。随着嵌套层级的加深,代码的可读性和可维护性会急剧下降。

  2. 错误处理复杂‌:
    在回调地狱中,每个异步操作可能都会失败,而错误处理通常需要在每个回调函数中单独进行。这不仅增加了代码的复杂度,也使得错误追踪和调试变得更加困难。

  3. 难以扩展和维护‌:
    随着业务逻辑复杂度的增加,回调地狱中的代码会变得难以理解和维护。添加新功能或修改现有逻辑可能会变得非常困难,因为需要仔细处理多层嵌套的回调函数和错误处理逻辑。

  4. 代码结构不清晰‌:
    回调地狱中的代码往往呈现出金字塔形状的结构,这使得代码的结构不清晰,难以阅读和理解。开发者需要花费更多的时间和精力来理解代码的执行流程和逻辑。

6.  为了避免回调地狱的问题,可以采用以下几种方法

  • 使用Promise‌(下面有示例):
    Promise是一种用于处理异步操作的对象,它可以将嵌套的回调转换为链式的.then调用,从而避免回调地狱。

  • 使用async/await‌(下面有示例):
    async/await是基于Promise的语法糖,它可以使异步代码看起来更像同步代码,进一步简化了异步编程。通过使用async/await,可以避免回调函数的嵌套,使代码更加简洁易读。

  • 模块化和分解‌:
    将复杂的业务逻辑拆分成更小的、独立的函数或方法,可以减少回调地狱的发生。每个函数或方法只处理一个特定的异步操作,并通过返回Promise来与其他函数或方法进行交互。

  • 使用响应式编程库‌:
    如Reactor或RxJava等响应式编程库提供了声明式的方式来处理异步流和事件,可以极大地简化复杂业务逻辑的处理,从而避免回调地狱的问题。

示例:先获取用户数据后才能获取订单数据

6.1  使用传统回调函数方式导致的回调地狱:

// 假设有两个函数,getUserData和getUserOrders,它们都是异步的并且接受回调函数
function getUserData(callback) {
    setTimeout(() => {
        // 异步操作完成后,调用回调函数并传递结果
        callback(null, { userId: 123, userName: 'John Doe' });
    }, 1000);
}

function getUserOrders(userId, callback) {
    setTimeout(() => {
        callback(null, [
            { orderId: 456, product: 'Laptop' }, 
            { orderId: 789, product: 'Smartphone' }
        ]);
    }, 1000);
}

// 使用回调函数方式获取用户数据和订单数据
getUserData((err, userData) => {
    if (err) {
        console.error('Error getting user data:', err);
        return;
    }
    // 用户数据获取成功后,会进行订单数据的获取
    getUserOrders(userData.userId, (err, orders) => {
        if (err) {
            console.error('Error getting user orders:', err);
            return;
        }
        console.log('User data:', userData);
        console.log('User orders:', orders);
    });
});

在这个例子中,有两个嵌套的回调函数。首先,我们调用getUserData来获取用户数据,然后在它的回调函数中调用getUserOrders来获取用户的订单数据。这种嵌套结构就是回调地狱的简化版。

使用async/await来重写上面的代码:

// 将异步函数转换为返回Promise的函数
// await 等待的是它右侧的一个表达式的返回值。这个表达式的计算结果是 Promise 对象或者其他值。
// await 只能用于 Promise,如果你尝试在非 Promise 上使用 await,它会导致运行时错误。

function getUserDataAsync() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve({ userId: 123, userName: 'John Doe' });
        }, 1000);
    });
}

function getUserOrdersAsync(userId) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve([{ orderId: 456, product: 'Laptop' }, { orderId: 789, product: 'Smartphone' }]);
        }, 1000);
    });
}

// 使用async/await方式获取用户数据和订单数据
async function fetchUserDataAndOrders() {
    try {
        const userData = await getUserDataAsync(); // 获取用户
        const orders = await getUserOrdersAsync(userData.userId); // 获取订单
        console.log('User data:', userData);
        console.log('User orders:', orders);
    } catch (err) {
        console.error('An error occurred:', err);
    }
}

fetchUserDataAndOrders();

6.2  使用Promise链式调用避免回调地狱

使用Promise是避免回调地狱的一种有效方法。Promise提供了一种更清晰、更线性的方式来处理异步操作,使得代码更易于阅读和维护。以下是如何使用Promise来避免回调地狱的步骤和示例:

  1. 将异步函数转换为返回Promise的函数‌:
    如果你有一个使用回调函数的异步函数,你可以将其改写为返回一个Promise的函数。这样,你就可以使用.then()链式调用来处理异步结果,而不是嵌套回调函数。

  2. 使用.then()链式调用‌:
    Promise的.then()方法允许你在Promise解决(resolve)后执行一个回调函数,并且这个回调函数可以返回一个新的Promise,从而允许你链式调用多个异步操作。

  3. 处理错误‌:
    使用.catch()方法来捕获Promise链中任何一步发生的错误,这样你就不需要在每个异步操作中都显式地处理错误了。

// 使用Promise方式的异步函数
function getUserData() {
    return new Promise((resolve, reject) => {
        // 模拟异步操作,比如网络请求或数据库查询
        setTimeout(() => {
            // 异步操作完成后,调用resolve传递结果
            resolve({ userId: 123, userName: 'John Doe' });
        }, 1000);
    });
}

function getUserOrders(userId) {
    return new Promise((resolve, reject) => {
        // 模拟异步操作
        setTimeout(() => {
            // 异步操作完成后,调用resolve传递结果
            resolve([
                { orderId: 456, product: 'Laptop' },
                { orderId: 789, product: 'Smartphone' }
            ]);
        }, 1000);
    });
}

// 使用Promise链式调用获取用户数据和订单数据
getUserData()
    .then(userData => {
        // 在获取到用户数据后,再获取用户订单数据
        return getUserOrders(userData.userId);
    })
    .then(orders => {
        // 成功获取到用户数据和订单数据
        console.log('User data:', userData); // 注意:这里的userData是从闭包中获取的,或者你可以在上一步中返回{ userData, orders }
        console.log('User orders:', orders);
    })
    .catch(err => {
        // 处理任何一步发生的错误
        console.error('Error:', err);
    });

在这个例子中,getUserDatagetUserOrders函数都返回了一个Promise。我们使用.then()方法来链式调用这两个异步操作,并在最后使用.catch()方法来捕获任何可能发生的错误。这样,我们的代码就避免了回调地狱,变得更加清晰和易于维护。

相关文章:

  • 【TOT】Tree-of-Thought Prompting
  • Esp32S3通过文心一言大模型实现智能语音对话
  • HarmonyOS进程通信及原理
  • 0x0000007b应用程序错误解决2
  • Kafka的生产者和消费者模型
  • 25/2/18 <算法笔记> ByteTrack
  • 赛博算命之 ”梅花易数“ 的 “JAVA“ 实现 ——从玄学到科学的探索
  • 【实用工具】基于Ubuntu的Docker加速镜像配置202502
  • QT数据库(三):QSqlQuery使用
  • AWS transit gateway 的作用
  • Qt的QTabWidget的使用
  • 深入理解Redis
  • word$deepseep
  • Qt开发③Qt的信号和槽_概念+使用+自定义信号和槽+连接方式
  • 跟着AI学vue第四章
  • Python 函数式编程全攻略:从理论到实战的深度解析
  • 基于指纹识别技术的考勤打卡设计与实现(论文+源码)
  • 七星棋牌全开源修复版源码解析:6端兼容,200种玩法全面支持
  • JavaScript系列(75)--代理模式专题
  • MySQL中count(1)和count(*) 的区别
  • 欧盟公布终止进口俄能源计划,2027年为最后期限
  • 网友建议平顶山请刘昊然任旅游宣传大使,市委:有此设想,正申请经费
  • “五一”假期全社会跨区域人员流动量超14.65亿人次
  • “五一”假期国内出游3.14亿人次,同比增长6.4%
  • 沙发上躺赢又如何?告别冠军绝缘体的凯恩,要开始收割荣誉了
  • 中海油高管调整:刘永杰、刘小刚任副总裁