掌握异步编程精髓:从回调地狱到promise再到async/await 的优雅进化
2025-01-02 09:20 阅读(168)

前言:

在我学习如何使用JS来拉取数据时,发现我虽然会使用fetch(),但是却对其原理并没有什么过多的了解,对其的了解仅限于其是基于promise对象而言,而当我对其搜索时发现最多的概念就是异步编程,所以本文来详细了解一下异步编程与其常见的形式。

异步编程的概念:

异步编程允许程序在等待某个长时间操作完成时,不阻塞或挂起执行的编程方式。

通俗来讲,就是异步编程允许执行某个长时间任务时,程序不需要等待,可以继续执行之后的代码,而当这个长时间任务完成后再来通知我们。

这种编程模式避免了程序的阻塞,也大大提升了CPU的执行效率。

以JavaScript为例的实现方式:

而为了实现异步编程当然少不了核心的几个方式:

一、回调函数:

传统的异步编程形式之一,通过传递一个函数作为参数,在异步操作完成后调用这个函数来处理结果。

例如使用setTimeout定时器来实现让一个函数在指定时间后执行,这样在等待的时候程序会继续运行其后面的代码。

setTimeout(() => {
    console.log("兄弟你好香")
}, 2000); // 两秒后执行打印

console.log("兄弟兄弟~")

setTimeout内部的函数本身会立刻返回,所以程序会紧接着执行之后的代码,而回调函数则需要等待指定时间后执行。

虽然回调很好理解,但是其有个明显的缺点


-->如果我们需要执行多个回调函数,那么代码很有可能会变成下面的样子:


setTimeout(() => {
    console.log("两秒后~");

    setTimeout(() => {
        console.log("两秒后~");

        setTimeout(() => {
            console.log("两秒后~");

            // .....
        }, 2000);
    }, 2000);
}, 2000);

在执行完第一个回调函数后又执行回调函数里面第二个任务,依次嵌套,而最直观的表现就是代码的可读性会变得很差,毕竟一直在向右增长。而这也是我们所熟知的函数的“回调地狱”。

二、Promise:

为了解决回调地狱的问题,promise诞生了。

在 JavaScript 中,promise 是一种处理异步操作的对象,其表示一个尚未完成但最终会完成或失败的操作的结果。每个 promise 都有三种状态:


pending(发出承诺):初始状态,既不是成功也不是失败。

fulfilled(已兑现):操作成功完成。

rejected(已拒绝):操作失败。


而使用promise的APIfetch()就是一个不错的例子,其用于发起一个请求来获取服务器数据。我们可以使用其来动态更新页面的内容,也就是Ajax技术。

当我们使用fetch()去访问一个测试地址的数据,我们可以发现其会返还一个promise对象。

fetch() 的行为遵循Promise(承诺)的概念——其承诺在未来的某个时刻兑现/拒绝提供数据。具体来说:


请求成功时,fetch 会解析为一个 response 对象,表示从服务器收到的响应。

如果请求因为网络问题而失败,promise 可能会被拒绝。(fetch() 只有在网络请求遇到问题时才会返回一个被拒绝的Promise,例如网络不可用、DNS查找失败等)


当 fetch() 的 promise 被解决为成功时,我们可以调用它的.then()方法来处理返还 response 对象并且使用回调函数来实现对数据的操作。

值得注意的是.then() 方法本身也返回一个新的 promise。

javascript 代码解读复制代码

fetch("https://.....")
// 就像常用的将返还内容转换为json对象
//  .then(res => res.json)
//  其具体为:
    .then((response) => {
        // 这里建议手动检查response.ok属性,并手动抛出错误
        return response.json(); // 返回一个promise,解析JSON数据完成后被解决
    })

正因.then返回一个新的 promise,所以使得其能够进行链式调用,使用一种链式结构来将多个异步操作串联起来,例如:

fetch("https://.....")
    .then(res => res.json())
    .then(data => console.log(data));
//  .then(() -> { ..... })    

我们可以使用上述代码将返回的数据转换为JSON格式完成后,再执行打印其结果,当然也可以在这一操作后再执行其他异步操作。

当然执行时可能会遇到一些错误,为了捕获这些错误,我们也可以在其末尾添加一个.catch。这样在之前的任意一个.then发生错误,控制权就会立刻转到.catch的身上。例如:

fetch("https://.....")
    .then(res => res.json())
    .then(data => console.log(data));
//  .then(() -> { ..... })   
    .catch((error) => {
        console.error(err);
    });

这样就避免了例如传统回调函数的层层嵌套,转变为了链式的向下增长,这样可读性就大大提升了。


三、async/await:

async/await是新添加的,基于Promise的一种语法糖,使异步代码看起来像同步代码,提高了代码的可读性和简洁性。

其使用方法也很简单,首先我们需要使用async来标记一个函数为异步函数(异步函数的返回值永远是promise对象)。

async function func() {
    // .....
}

func(); // 返还值恒为promise对象

我们可以在异步函数中调用其他的异步函数,不过不再需要使用.then(),而是使用await语法,例如:

async function func() {
    // 当遇到await时,js引擎会停止async函数的执行,直到await后面的promise被解析
    // 并且直接返回结果,所以这里的response已经是服务器返回的数据了
    const response = await fetch("http://.....");
    const data = await response.json();
    console.log(data);
}

func();

看似在此处await停止了函数的执行,但是在等待promise解析的过程中,JS仍然可以处理其它任务。


需要注意的是,await不能单独存在,其是强制依赖于async函数的,其只能在async函数内部存在。