likes
comments
collection
share

解锁JavaScript异步奥秘:Promise与async/await的魔法之旅

作者站长头像
站长
· 阅读数 34

什么是异步

异步是一种编程模型,与同步相对。在异步编程中,程序不会等待某个操作(如I/O操作、网络请求等)完成后再继续执行下一行代码,而是继续执行后续任务,无需阻塞或等待。这种方式提高了程序的效率和响应性,特别是在执行耗时操作时,能够确保用户界面或其他任务流畅进行。

下面我将以一段代码来为大家展示什么是异步:

function foo() {
    setTimeout(() => {
        console.log("hello")
    }, 1000)
}

function bar() {
    console.log("world")
}

foo();
bar();

输出结果;

解锁JavaScript异步奥秘:Promise与async/await的魔法之旅

为什么这段代码是先输出world再输出hello?

  • js是单线程执行的,当遇到需要耗时的异步代码时会先将该代码挂起,再去执行后续不耗时的同步代码
  • 上述代码为例,当执行到foo时V8引擎判断该函数为需要耗时的异步代码先将该代码挂起
  • 当执行到bar时,V8引擎判断该函数为不耗时的同步代码将该函数执行,当该函数执行完毕后再去执行foo函数,所以会先输出world再输出hello

在早期编写异步函数的过程中会出现多层嵌套的回调函数,这种结构非常的难理解和阅读,随着嵌套层数的增加维护会变得很难,这也就是所谓的回调地狱。如以下代码:

function getUser(id, callback) {
    setTimeout(() => {
        console.log(`Getting user with id ${id}`);
        callback({ id: id, name: 'John Doe' });
    }, 1000);
}

function getOrders(userId, callback) {
    setTimeout(() => {
        console.log(`Getting orders for user with id ${userId}`);
        callback([{ orderId: 1, product: 'Book' }, { orderId: 2, product: 'CD' }]);
    }, 1500);
}

function getOrderDetails(orderId, callback) {
    setTimeout(() => {
        console.log(`Getting details for order with id ${orderId}`);
        callback({ orderId: orderId, details: 'Shipping to: 123 Street' });
    }, 2000);
}

getUser(1, (user) => {
    getOrders(user.id, (orders) => {
        const firstOrderId = orders[0].orderId;
        getOrderDetails(firstOrderId, (orderDetails) => {
            console.log(`User ${user.name}'s first order details:`, orderDetails);
        });
    });
});

在这个例子中,我们首先获取一个用户,然后根据用户ID获取其订单,最后获取第一个订单的详情。每个步骤都是异步的,并且依赖于前一个步骤的结果,导致了多层嵌套的回调函数。这种结构不仅难以阅读和理解,而且随着逻辑复杂性的增加,维护和调试也会变得非常困难,这就是所谓的回调地狱

为了解决多层回调函数的窘境,在es6推出了一种全新的编写异步代码的方式---Promise

Promise

Promise(字面意思承诺),白话理解就是给调用者一个承诺,一段时间后就会将数据返回给你,就可以创建一个promise对象。

  • 当new了一个Promise时,此时需要传递一个回调函数,这个函数为立即执行的,叫做(executor)
  • 在这个回调函数中,需要传入两个参数,reslove,reject
  • 执行reslove函数时,回调Promise对象的.then函数
  • 执行了reject函数时,回调Promise对象的.catch函数

1. executor执行器

Promise的构造函数是用来实例化一个新的Promise对象的,它接受一个函数作为参数,这个函数我们称为执行器(executor)。执行器函数在Promise创建时立即执行。如以下代码;

let foo = new Promise((resolve, reject) => {
    console.log('hello world'); // hello world
})

从这段代码可以看出传入的执行器函数executor是会在创建Promise对象时立即执行的。

2.Promise的状态

Promise有三种状态,这些状态描述了Promise从创建到最终完成(或失败)的过程。

  • 首先给大家举一个例子更好的理解这三种状态。
    • 你和好兄弟约好了周末一起开黑,但是现在还没有到周末,此时的状态就为等待中
    • 转眼间到了周末,你和好兄弟成功开黑,此时的状态为成功
    • 到了周末,你的女朋友临时要你陪她去逛街,无奈之下你只能放好兄弟的鸽子不和他们开黑了,此时的状为拒绝
  1. Pending(等待中) :这是Promise初始状态。当一个Promise刚被创建执行了executor但其内部的异步操作还未完成(既没有被resolve解决,也没有被reject拒绝)时,Promise处于等待中状态。
  2. Fulfilled(成功) :当Promise中的操作成功完成,通过调用resolve函数将Promise的状态置为成功状态。一旦Promise变为fulfilled状态,它就不能再变为其他状态,同时会传递一个可选的成功值给到接下来的.then方法中的回调函数。
  3. Rejected(拒绝) :如果Promise中的操作失败或遇到错误,通过调用reject函数将Promise的状态置为拒绝状态。同样地,一旦Promise变为rejected状态,它就会保持这个状态不变,并且会传递一个可选的错误原因给到.catch方法中的回调函数处理。

Promise的状态转换规则:

  1. Promise只能从Pending转换到FulfilledRejected,不能逆向转换,也不能多次转换。
  2. 一旦Promise变为FulfilledRejected状态,其内部的值(成功的结果或失败的原因)就被固定了,之后的任何操作都不会改变这个值,即.then.catch中的回调函数在Promise状态改变后任何时候调用都会得到同样的结果。

优点: 这些状态机的设计使得Promise能够以一种确定性的方式处理异步操作的结果,简化了异步编程的复杂度。

.then接收参数

then方法可以接受两种不同类型参数,一种参数为成功的回调,另一种参数为失败的回调,如下实例:

const promise = new Promise((resolve, reject) => {
    resolve('success')
    // reject('error')
})

promise.then(res => console.log(res), rej => console.log(rej)) //success
const promise = new Promise((resolve, reject) => {
    // resolve('success')
    reject('error')
})

promise.then(res => console.log(res), rej => console.log(rej)) //error

.catch方法

catch方法有返回值,它的返回值是Promise

返回普通对象

let promise = new Promise((resolve, reject) => {
    reject('error')
})

promise.catch(err => ({ text: 'hello' })).then(res => console.log(res)) // { text: 'hello' }

返回Promise

let promise = new Promise((resolve, reject) => {
    reject('error')
})

promise.catch(err => {
    return new Promise((resolve, reject) => {
        reject('第二个错误promise')
    })
}).catch(res => console.log(res))
// 第二个错误promise

在这段代码中第一次catch返回了一个Promise对象,该对象reject的内容又会被catch捕捉到。

3.Promise解决回调地狱问题

为了使用Promise解决上述代码中的回调地狱问题,我们可以将每个异步操作封装成返回Promise的函数,然后通过.then链式调用来组织逻辑。下面是转换后的代码示例:

function getUser(id) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(`Getting user with id ${id}`);
            resolve({ id: id, name: 'John Doe' });
        }, 1000);
    });
}

function getOrders(userId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(`Getting orders for user with id ${userId}`);
            resolve([{ orderId: 1, product: 'Book' }, { orderId: 2, product: 'CD' }]);
        }, 1500);
    });
}

function getOrderDetails(orderId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(`Getting details for order with id ${orderId}`);
            resolve({ orderId: orderId, details: 'Shipping to: 123 Street' });
        }, 2000);
    });
}

getUser(1)
    .then(user => getOrders(user.id))
    .then(orders => getOrderDetails(orders[0].orderId))
    .then(orderDetails => {
        console.log(`User ${user.name}'s first order details:`, orderDetails);
    })
    .catch(error => {
        console.error('Error caught:', error);
    });

在这个版本中,每个原本接收回调函数的异步操作现在都返回一个Promise。通过链式调用.then,我们可以依次执行这些操作,而不需要嵌套的回调函数,使得代码更加扁平化和易于阅读。此外,还添加了一个.catch来统一处理可能发生的错误。

async/await

虽然在es6中推出了Promise的用法,但是在某些大量函数嵌套的场景下使用Promise仍然不够优雅,于是在es7提出了async/await函数,这次终于让JS的异步操作变得无比的优雅。

什么是async/await

async/await 是 JavaScript 中用于处理异步操作的两个关键字,它们简化了基于Promise的异步编程模型,使得异步代码更加易于理解和维护。

  1. async: 这个关键字用于声明一个异步函数。声明为 async 的函数会隐式地返回一个 Promise 对象(async是Promise的语法糖)。在异步函数内部,可以使用 await 关键字。
  2. await: 该关键字只能在 async 函数中使用。它用于等待一个 Promise 完成(解析或拒绝)。await 使得你可以以同步的方式编写异步代码,即代码会暂停在 await 处,直到等待的 Promise 完成,然后继续执行并返回Promise的结果。

状态为fulfilled

let data = null;

function getData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            data = [1, 2, 3]
            console.log(data)
            resolve()
        }, 1000)
    })
}

function another() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            data.push(4);
            resolve()
        }, 1000)
    })
}

function another2() {
    console.log(data)
}


// async 写了 默认会返回 new Promise
async function foo() {
    await getData();
    await another();
    another2();
}

foo(); 

运行结果为:

解锁JavaScript异步奥秘:Promise与async/await的魔法之旅

在上述代码的例子中我们可以看到,await后面的代码,其实相当于.then的回调,当状态变为fulfilled时才会继续执行后面的代码。

状态为rejected

function requestData(url) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (url.includes('hello')) {
                resolve(url)
            } else {
                reject('请求错误')
            }
        }, 1000);
    })
}

async function getData() {
    try {
        const res = await requestData('world')
        console.log(res)
    } catch (error) {
        console.error(error); // 在这里处理错误
    }
}

getData()

// 请求错误

通过添加 try...catch 块,当 requestData 函数中的 Promise 被拒绝时,错误会被捕获并在 catch 块中处理,这样就不会导致程序中断并能够优雅地处理错误情况。

总结

本篇文章就到此为止啦,希望通过这篇文章能对你理解JS异步编程有所帮助,本人水平有限难免会有纰漏,欢迎大家指正。如觉得这篇文章对你有帮助的话,欢迎点赞收藏加关注,感谢支持🌹🌹。

解锁JavaScript异步奥秘:Promise与async/await的魔法之旅

转载自:https://juejin.cn/post/7380994802745229349
评论
请登录