深入理解 Promise 与 Async/Await:异步编程不再难

194 阅读5分钟

在 JavaScript 编程的世界里,异步操作是一个核心且常见的场景。无论是网络请求、文件读取还是定时器操作,都涉及到异步处理。今天,我们就来深入探讨 JavaScript 中处理异步操作的两个强大工具:Promise 和 Async/Await。

异步编程基础

在开始介绍 Promise 和 Async/Await 之前,我们先了解一下异步编程的基础知识。

同步任务与异步任务

JavaScript 中的任务可以分为同步任务和异步任务。同步任务是按照代码编写的顺序依次执行的,一个任务执行完才会执行下一个任务。而异步任务则不同,它不会阻塞后续代码的执行,当遇到异步任务时,JavaScript 会先跳过该任务,继续执行后面的代码,等异步任务完成后再回过头来处理它的结果。

例如,在下面的代码中:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        // 异步任务
        setTimeout(() => {
            console.log('hello1');
        }, 3000);
        // 同步任务
        var a = 1;
        console.log('hello2');
    </script>
</body>
</html>

setTimeout 是一个异步任务,当代码执行到 setTimeout 时,它会将回调函数注册到事件队列中,然后继续执行后面的同步代码。所以,会先输出 hello2,3 秒后才会输出 hello1

image.png

CPU 轮询

由于计算机中的 CPU 资源是有限的,而同时可能有很多应用程序在运行,为了让每个程序都能得到执行的机会,CPU 会采用轮询的方式来分配资源。就像漫画和动画片,1 秒钟需要显示 20 帧以上才能让我们感觉是连贯的画面,CPU 也是在一个单位时间内不断切换执行不同的任务,只要切换速度足够快,我们就感觉所有程序都在同时运行。

Promise 的诞生与使用

Promise 解决的问题

在传统的异步编程中,代码的可读性和可维护性往往是一个大问题。因为异步任务的执行顺序和代码的编写顺序不一致,导致代码的逻辑变得复杂。例如,当我们需要依次执行多个异步任务时,就会出现所谓的“回调地狱”,代码会嵌套得很深,难以理解和维护。

Promise 的出现就是为了解决这个问题。它可以让我们以一种更优雅的方式来处理异步操作,使代码的逻辑更加清晰。

Promise 的基本使用

Promise 是一个构造函数,通过 new Promise() 可以创建一个 Promise 实例。它接受一个执行器函数作为参数,这个执行器函数又接受两个参数:resolvereject,分别用于将 Promise 的状态从 pending(进行中)变为 fulfilled(已成功)和 rejected(已失败)。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        const p = new Promise((resolve) => {
            console.log('hello'); // 立即执行
            setTimeout(() => {
                console.log('hello1');
                resolve();
            }, 10);
        });
        p.then(() => {
            console.log('hello2');
        });
    </script>
</body>
</html>

在这个例子中,我们创建了一个 Promise 实例 p,在执行器函数中,console.log('hello') 是同步代码,会立即执行。然后 setTimeout 是一个异步任务,10 毫秒后会执行回调函数,在回调函数中调用 resolve() 方法将 Promise 的状态变为 fulfilledp.then() 方法会在 Promise 的状态变为 fulfilled 时执行回调函数,所以会输出 hello2

image.png

控制执行流程

使用 Promise 可以很方便地控制异步任务的执行顺序。例如,我们可以通过链式调用 then 方法来依次执行多个异步任务。

const readFilePromise = new Promise((resolve) => {
    const fs = require('fs');
    fs.readFile('./1.html', (err, data) => {
        console.log(data.toString());
        resolve();
    });
});
readFilePromise.then(() => {
    console.log('读完了');
});
console.log('123');

在这个例子中,我们创建了一个 Promise 来读取文件内容,当文件读取完成后,调用 resolve() 方法,然后在 then 方法中输出 读完了

image.png

Async/Await:Promise 的升级版

Async/Await 的基本概念

虽然 Promise 已经让异步编程变得更加优雅,但在处理多个异步任务的链式调用时,代码仍然会显得有些冗长。Async/Await 是 ES2017 引入的新特性,它是基于 Promise 实现的,让异步代码看起来更像同步代码,进一步提高了代码的可读性和可维护性。

Async/Await 的使用

async 关键字用于修饰函数,表示这是一个异步函数。异步函数总是返回一个 Promise 对象。await 关键字只能在 async 函数内部使用,它会暂停异步函数的执行,等待后面的 Promise 对象状态变为 fulfilled 后,再继续执行异步函数,并返回 Promise 的结果。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
<script>
( async function() {
  const p = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('success')
    }, 1000)
  })
  const res = await p
  console.log(res)
  console.log('hello')
})()
</script>
</body>
</html>

在这个例子中,我们定义了一个立即执行的异步函数,在函数内部创建了一个 Promise 对象 p,使用 await 关键字等待 p 的状态变为 fulfilled,然后将结果赋值给 res 并输出。最后输出 hello

image.png

实际应用场景

在实际开发中,我们经常会遇到需要进行网络请求的场景,使用 Async/Await 可以让代码更加简洁明了。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <ul id="repos"></ul>
    <button type="button" id="btn">加载</button>
    <script>
        document.addEventListener('DOMContentLoaded', async () => {
            const res = await fetch('https://api.github.com/users/githubnumber/repos');
            const data = await res.json();
            console.log(data);
            document.getElementById('repos').innerHTML = data.map(item => {
                return `<li>
                   <a href="${item.html_url}" target="_blank">${item.name}</a>
                    </li>`;
            }).join('');
        });
    </script>
</body>
</html>

在这个例子中,我们使用 async/await 来处理网络请求,代码看起来就像同步代码一样,非常清晰。

image.png

总结

Promise 和 Async/Await 是 JavaScript 中处理异步操作的两个重要工具。Promise 解决了传统异步编程中回调地狱的问题,让代码的逻辑更加清晰。而 Async/Await 则是在 Promise 的基础上进一步简化了异步代码的编写,让异步代码看起来更像同步代码。掌握这两个工具,可以让我们在处理异步操作时更加得心应手。希望通过本文的介绍,你对 Promise 和 Async/Await 有了更深入的理解。