MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

async和await在JavaScript异步编程中的高级用法

2023-10-026.3k 阅读

深入理解 async 和 await 的基础原理

异步编程背景与 Promise

在 JavaScript 中,异步编程是处理非阻塞 I/O 操作、网络请求、定时任务等场景的关键技术。早期,我们使用回调函数来实现异步操作,但回调地狱(Callback Hell)的问题使得代码的可读性和维护性急剧下降。例如:

getData((data1) => {
    processData1(data1, (data2) => {
        processData2(data2, (data3) => {
            // 更多嵌套
        });
    });
});

Promise 的出现极大地改善了这种情况。Promise 是一个代表异步操作最终完成(或失败)及其结果值的对象。它有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。通过链式调用 then 方法,我们可以更优雅地处理异步操作的结果。例如:

getData()
  .then(processData1)
  .then(processData2)
  .catch((error) => {
        console.error('An error occurred:', error);
    });

async 和 await 的本质

async 函数是一种异步函数,它返回一个 Promise 对象。如果 async 函数的返回值不是一个 Promise,JavaScript 会自动将其包装成一个已解决(resolved)状态的 Promise。例如:

async function simpleAsyncFunction() {
    return 'Hello, async!';
}
simpleAsyncFunction().then((result) => {
    console.log(result); // 输出: Hello, async!
});

await 关键字只能在 async 函数内部使用,它用于暂停 async 函数的执行,等待一个 Promise 对象的解决(resolved)或拒绝(rejected)。当 await 一个 Promise 时,async 函数会暂停执行,直到该 Promise 被解决(resolved),然后 await 表达式会返回该 Promise 的解决值。例如:

async function asyncWithAwait() {
    const promise = new Promise((resolve) => {
        setTimeout(() => {
            resolve('Resolved after 1 second');
        }, 1000);
    });
    const result = await promise;
    console.log(result); // 输出: Resolved after 1 second
}
asyncWithAwait();

本质上,asyncawait 是基于 Promise 构建的语法糖,使得异步代码看起来更像同步代码,大大提高了代码的可读性和可维护性。

async 和 await 的高级应用场景

处理多个并发异步操作

Promise.all 的结合使用

在实际开发中,我们经常需要同时发起多个异步请求,并在所有请求都完成后进行下一步操作。这时候可以结合 Promise.allasync/awaitPromise.all 接受一个 Promise 对象数组作为参数,并返回一个新的 Promise,这个新的 Promise 在所有输入的 Promise 都被解决(resolved)时才会被解决(resolved),其解决值是一个包含所有输入 Promise 解决值的数组。例如,我们有三个异步函数 fetchData1fetchData2fetchData3,分别模拟不同的数据获取操作:

function fetchData1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Data from fetchData1');
        }, 1000);
    });
}
function fetchData2() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Data from fetchData2');
        }, 1500);
    });
}
function fetchData3() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Data from fetchData3');
        }, 2000);
    });
}
async function concurrentFetch() {
    const [data1, data2, data3] = await Promise.all([fetchData1(), fetchData2(), fetchData3()]);
    console.log(data1, data2, data3);
    // 输出: Data from fetchData1 Data from fetchData2 Data from fetchData3
}
concurrentFetch();

在上述代码中,Promise.all 同时发起了三个异步操作,await 等待所有操作完成后,将结果分别赋值给 data1data2data3

Promise.race 的结合使用

Promise.race 同样接受一个 Promise 对象数组作为参数,并返回一个新的 Promise。但与 Promise.all 不同的是,Promise.race 会在数组中的任何一个 Promise 被解决(resolved)或被拒绝(rejected)时,立即返回这个 Promise 的状态和值。例如:

function fastFetch() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Fast data');
        }, 500);
    });
}
function slowFetch() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Slow data');
        }, 2000);
    });
}
async function raceFetch() {
    const result = await Promise.race([fastFetch(), slowFetch()]);
    console.log(result); // 输出: Fast data
}
raceFetch();

在这个例子中,fastFetch 函数会更快地解决(resolved),因此 Promise.race 会返回 fastFetch 的解决值。

错误处理与异常捕获

try - catch 块的使用

async 函数中,使用 try - catch 块可以优雅地捕获异步操作中抛出的错误。与 Promise 的 catch 方法不同,try - catch 可以捕获 await 表达式中的错误,使得错误处理更加集中和直观。例如:

async function asyncWithError() {
    try {
        const result = await new Promise((_, reject) => {
            setTimeout(() => {
                reject(new Error('Simulated error'));
            }, 1000);
        });
        console.log(result);
    } catch (error) {
        console.error('Caught error:', error.message);
        // 输出: Caught error: Simulated error
    }
}
asyncWithError();

在上述代码中,await 表达式中的 Promise 被拒绝(rejected),try - catch 块捕获到这个错误并进行处理。

自定义错误处理函数

我们还可以将错误处理逻辑封装成一个函数,以便在多个 async 函数中复用。例如:

function handleError(error) {
    console.error('Custom error handling:', error.message);
}
async function anotherAsyncWithError() {
    try {
        const result = await new Promise((_, reject) => {
            setTimeout(() => {
                reject(new Error('Another simulated error'));
            }, 1500);
        });
        console.log(result);
    } catch (error) {
        handleError(error);
        // 输出: Custom error handling: Another simulated error
    }
}
anotherAsyncWithError();

通过这种方式,我们可以统一管理和维护错误处理逻辑,提高代码的可维护性。

实现异步迭代器与生成器

异步迭代器基础

异步迭代器是一种可以异步产生一系列值的对象,它具有 next() 方法,该方法返回一个 Promise,这个 Promise 解决(resolved)后的值是一个包含 valuedone 属性的对象,类似于同步迭代器。例如,我们有一个异步生成数据的函数 asyncDataGenerator

async function asyncDataGenerator() {
    let value = 0;
    return {
        async next() {
            await new Promise((resolve) => setTimeout(resolve, 1000));
            if (value < 3) {
                return { value: value++, done: false };
            }
            return { done: true };
        }
    };
}
async function consumeAsyncIterator() {
    const iterator = await asyncDataGenerator();
    let result = await iterator.next();
    while (!result.done) {
        console.log(result.value);
        result = await iterator.next();
    }
}
consumeAsyncIterator();
// 每隔1秒输出: 0, 1, 2

在上述代码中,asyncDataGenerator 返回一个异步迭代器,consumeAsyncIterator 函数通过 await 等待 next() 方法返回的 Promise,逐步获取迭代器的值。

与 async/await 结合的异步生成器

异步生成器是一种特殊的函数,它使用 async function* 语法定义,可以异步生成一系列值。异步生成器函数返回一个异步迭代器对象。例如:

async function* asyncGenerator() {
    yield await new Promise((resolve) => setTimeout(() => resolve('First value'), 1000));
    yield await new Promise((resolve) => setTimeout(() => resolve('Second value'), 1500));
    yield await new Promise((resolve) => setTimeout(() => resolve('Third value'), 2000));
}
async function consumeAsyncGenerator() {
    const generator = asyncGenerator();
    let result = await generator.next();
    while (!result.done) {
        console.log(result.value);
        result = await generator.next();
    }
}
consumeAsyncGenerator();
// 每隔一段时间输出: First value, Second value, Third value

在这个例子中,asyncGenerator 是一个异步生成器函数,yield 关键字暂停函数执行并返回一个值,await 用于等待 Promise 解决(resolved)。consumeAsyncGenerator 函数通过迭代异步生成器来获取生成的值。

性能优化与注意事项

避免不必要的等待

在使用 await 时,要注意避免不必要的等待,尤其是在多个异步操作之间没有依赖关系的情况下。例如,如果有两个独立的异步函数 asyncTask1asyncTask2,我们可以同时发起这两个任务,而不是依次等待它们完成。比较以下两种方式:

依次等待

async function sequentialTasks() {
    const result1 = await asyncTask1();
    const result2 = await asyncTask2();
    return [result1, result2];
}

并发执行

async function concurrentTasks() {
    const task1 = asyncTask1();
    const task2 = asyncTask2();
    return [await task1, await task2];
}

concurrentTasks 中,asyncTask1asyncTask2 同时开始执行,而在 sequentialTasks 中,asyncTask2 要等到 asyncTask1 完成后才开始执行。在任务较多且耗时较长的情况下,并发执行可以显著提高性能。

内存管理与资源释放

在异步操作中,尤其是涉及到网络请求、文件操作等资源的使用时,要注意及时释放资源,避免内存泄漏。例如,在使用 fetch 进行网络请求时,如果请求失败或取消,要确保相关的资源被正确释放。可以使用 AbortController 来实现请求的取消:

async function fetchDataWithAbort() {
    const controller = new AbortController();
    const signal = controller.signal;
    try {
        const response = await fetch('https://example.com/api/data', { signal });
        const data = await response.json();
        return data;
    } catch (error) {
        if (error.name === 'AbortError') {
            console.log('Request aborted');
        } else {
            console.error('An error occurred:', error);
        }
    }
    // 无论请求成功或失败,这里可以进行资源清理等操作
}
// 可以在需要时调用 controller.abort() 取消请求

在上述代码中,AbortController 用于控制请求的取消,signal 传递给 fetch 方法。如果请求被取消,catch 块会捕获到 AbortError 并进行相应处理。

代码结构与可读性

虽然 async/await 使得异步代码更像同步代码,但随着项目规模的增大,代码结构的合理性变得尤为重要。建议将复杂的异步逻辑封装成独立的函数,避免在一个 async 函数中编写过多的业务逻辑。例如:

async function complexAsyncOperation() {
    const step1Result = await step1();
    const step2Result = await step2(step1Result);
    const step3Result = await step3(step2Result);
    return step3Result;
}
function step1() {
    return new Promise((resolve) => {
        // 模拟异步操作
        setTimeout(() => {
            resolve('Step 1 result');
        }, 1000);
    });
}
function step2(input) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(`${input} processed in step 2`);
        }, 1500);
    });
}
function step3(input) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(`${input} processed in step 3`);
        }, 2000);
    });
}

通过这种方式,每个步骤的逻辑清晰,易于维护和调试。同时,合理使用注释和命名规范也能提高代码的可读性。

async 和 await 在不同环境中的应用

在 Node.js 中的应用

文件系统操作

在 Node.js 中,fs 模块提供了文件系统操作的功能。早期,这些操作大多是基于回调函数的异步操作,而现在可以使用 async/await 结合 Promise 化的 fs 方法来进行更优雅的文件操作。例如,读取一个文件的内容:

const fs = require('fs').promises;
async function readFileContent() {
    try {
        const data = await fs.readFile('example.txt', 'utf8');
        console.log(data);
    } catch (error) {
        console.error('Error reading file:', error);
    }
}
readFileContent();

在上述代码中,fs.readFile 被 Promise 化,通过 await 等待文件读取操作完成,避免了回调函数的嵌套。

网络服务器开发

在 Node.js 开发网络服务器时,async/await 可以简化处理请求和响应的异步逻辑。例如,使用 http 模块创建一个简单的服务器:

const http = require('http');
const fs = require('fs').promises;
async function handleRequest(req, res) {
    try {
        const data = await fs.readFile('index.html', 'utf8');
        res.writeHead(200, { 'Content-Type': 'text/html' });
        res.end(data);
    } catch (error) {
        res.writeHead(500, { 'Content-Type': 'text/plain' });
        res.end('Error occurred');
    }
}
const server = http.createServer(handleRequest);
server.listen(3000, () => {
    console.log('Server running on port 3000');
});

在这个例子中,handleRequest 函数使用 async/await 处理文件读取和响应发送,使得服务器代码逻辑更加清晰。

在浏览器中的应用

处理 DOM 操作与动画

在浏览器环境中,async/await 可以用于处理与 DOM 操作相关的异步任务,例如等待动画完成后执行下一步操作。假设我们有一个动画元素,通过 CSS 动画使其移动,我们可以使用 requestAnimationFrameasync/await 来实现动画完成后的操作:

<!DOCTYPE html>
<html lang="en">

<head>
    <style>
        #animated-element {
            width: 100px;
            height: 100px;
            background-color: blue;
            animation: move 5s forwards;
        }

        @keyframes move {
            from {
                transform: translateX(0);
            }

            to {
                transform: translateX(500px);
            }
        }
    </style>
</head>

<body>
    <div id="animated-element"></div>
    <script>
        async function waitForAnimation() {
            return new Promise((resolve) => {
                const element = document.getElementById('animated - element');
                element.addEventListener('animationend', resolve);
            });
        }
        async function performAfterAnimation() {
            await waitForAnimation();
            const newElement = document.createElement('p');
            newElement.textContent = 'Animation completed';
            document.body.appendChild(newElement);
        }
        performAfterAnimation();
    </script>
</body>

</html>

在上述代码中,waitForAnimation 函数返回一个 Promise,等待动画结束后解决(resolved),performAfterAnimation 函数使用 await 等待动画完成后添加一个新的 DOM 元素。

处理 AJAX 请求

在浏览器中进行 AJAX 请求时,fetch API 结合 async/await 是一种常用的方式。例如,发送一个 GET 请求并处理响应:

async function fetchData() {
    try {
        const response = await fetch('https://example.com/api/data');
        if (!response.ok) {
            throw new Error('Network response was not ok');
        }
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('Error fetching data:', error);
    }
}
fetchData().then((result) => {
    console.log(result);
});

在这个例子中,fetch 返回一个 Promise,通过 await 等待响应,然后处理响应数据并进行错误处理。

与其他异步编程技术的对比

与回调函数的对比

代码可读性

回调函数在处理多个异步操作时,容易出现回调地狱,代码的嵌套层次过多,导致可读性变差。例如:

getData((data1) => {
    processData1(data1, (data2) => {
        processData2(data2, (data3) => {
            // 更多嵌套
        });
    });
});

async/await 使得异步代码看起来更像同步代码,提高了可读性。例如:

async function asyncDataProcessing() {
    const data1 = await getData();
    const data2 = await processData1(data1);
    const data3 = await processData2(data2);
    return data3;
}

错误处理

在回调函数中,错误处理通常需要在每个回调函数内部进行,增加了代码的复杂性。例如:

getData((data1, error1) => {
    if (error1) {
        return console.error('Error in getData:', error1);
    }
    processData1(data1, (data2, error2) => {
        if (error2) {
            return console.error('Error in processData1:', error2);
        }
        processData2(data2, (data3, error3) => {
            if (error3) {
                return console.error('Error in processData2:', error3);
            }
        });
    });
});

而在 async/await 中,可以使用 try - catch 块统一捕获错误,使得错误处理更加简洁。例如:

async function asyncErrorHandling() {
    try {
        const data1 = await getData();
        const data2 = await processData1(data1);
        const data3 = await processData2(data2);
        return data3;
    } catch (error) {
        console.error('An error occurred:', error);
    }
}

与 Promise 的对比

语法简洁性

Promise 通过链式调用 then 方法来处理异步操作的结果,虽然比回调函数有很大改进,但相比 async/await,语法上还是略显繁琐。例如,使用 Promise 进行多个异步操作的链式调用:

getData()
  .then(processData1)
  .then(processData2)
  .then((data3) => {
        console.log(data3);
    })
  .catch((error) => {
        console.error('An error occurred:', error);
    });

async/await 以更接近同步代码的方式编写异步逻辑,语法更简洁。例如:

async function asyncChaining() {
    const data1 = await getData();
    const data2 = await processData1(data1);
    const data3 = await processData2(data2);
    console.log(data3);
}

代码执行流程

Promise 的链式调用使得代码的执行流程是基于事件循环和回调队列的,在阅读代码时,需要关注链式调用的顺序和每个 then 方法的执行时机。而 async/await 代码的执行流程更直观,类似于同步代码的顺序执行,只是在遇到 await 时暂停,等待 Promise 解决(resolved)后继续执行。

实际项目中的案例分析

前端项目中的数据获取与渲染

假设我们正在开发一个博客网站的前端页面,需要从 API 获取文章列表并渲染到页面上。我们可以使用 async/await 结合 fetch 来实现:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF - 8">
    <meta name="viewport" content="width=device - width, initial - scale = 1.0">
    <title>Blog Page</title>
</head>

<body>
    <ul id="article - list"></ul>
    <script>
        async function fetchArticles() {
            try {
                const response = await fetch('https://example.com/api/articles');
                if (!response.ok) {
                    throw new Error('Network response was not ok');
                }
                const articles = await response.json();
                return articles;
            } catch (error) {
                console.error('Error fetching articles:', error);
            }
        }
        async function renderArticles() {
            const articles = await fetchArticles();
            const list = document.getElementById('article - list');
            articles.forEach((article) => {
                const listItem = document.createElement('li');
                listItem.textContent = article.title;
                list.appendChild(listItem);
            });
        }
        renderArticles();
    </script>
</body>

</html>

在这个例子中,fetchArticles 函数使用 async/await 从 API 获取文章数据,renderArticles 函数等待数据获取完成后将文章标题渲染到页面上。

后端项目中的数据库操作与业务逻辑处理

在一个 Node.js 的后端项目中,假设我们使用 MongoDB 作为数据库,需要处理用户注册的业务逻辑。我们可以使用 async/await 结合 MongoDB 的驱动来实现:

const { MongoClient } = require('mongodb');
async function registerUser(userData) {
    const uri = "mongodb://localhost:27017";
    const client = new MongoClient(uri);
    try {
        await client.connect();
        const database = client.db('my - database');
        const usersCollection = database.collection('users');
        const result = await usersCollection.insertOne(userData);
        return result;
    } catch (error) {
        console.error('Error registering user:', error);
    } finally {
        await client.close();
    }
}
// 调用示例
const user = { username: 'testuser', password: 'testpassword' };
registerUser(user).then((result) => {
    console.log('User registered:', result);
});

在上述代码中,registerUser 函数使用 async/await 连接 MongoDB 数据库,插入用户数据,并在最后关闭数据库连接。通过这种方式,我们可以清晰地处理数据库操作和业务逻辑,同时进行错误处理。

综上所述,asyncawait 在 JavaScript 异步编程中提供了强大而优雅的解决方案,无论是在前端还是后端开发中,都能显著提高代码的可读性、可维护性和性能。通过深入理解其原理和高级应用场景,开发者可以更好地利用这一技术,打造出高效、稳定的应用程序。在实际项目中,合理运用 async/await,结合其他相关技术,能够有效应对各种异步编程挑战,提升项目的整体质量。