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

JavaScript async和await的基本用法

2021-01-094.5k 阅读

1. 异步编程背景

在JavaScript中,由于JavaScript是单线程运行的,这意味着它在同一时间只能执行一个任务。如果遇到一个耗时较长的操作,例如网络请求、文件读取等,就会阻塞后续代码的执行,导致页面出现卡顿,用户体验变差。为了解决这个问题,JavaScript引入了异步编程的概念。

早期,JavaScript使用回调函数(callback)来处理异步操作。例如,在进行一个简单的setTimeout操作时,就会用到回调函数:

setTimeout(function () {
    console.log('Hello, this is a callback');
}, 1000);

但是,当异步操作变得复杂,多个异步操作相互依赖时,就会出现回调地狱(callback hell)的问题。例如:

getData((data1) => {
    processData1(data1, (data2) => {
        processData2(data2, (data3) => {
            processData3(data3, (result) => {
                console.log(result);
            });
        });
    });
});

这种层层嵌套的代码结构不仅难以阅读,而且维护成本极高。

后来,Promise的出现改善了这种情况。Promise是一个表示异步操作最终完成(或失败)及其结果值的对象。它通过.then()方法链式调用,使得代码结构更加清晰:

getData()
  .then(processData1)
  .then(processData2)
  .then(processData3)
  .then((result) => console.log(result));

然而,Promise链式调用虽然解决了回调地狱的问题,但对于非常复杂的异步流程,代码依然不够简洁。这时,asyncawait的出现进一步优化了异步编程。

2. async函数

async函数是ES2017引入的异步函数,它是基于Promise的语法糖,使得异步代码看起来更像同步代码,极大地提高了代码的可读性和可维护性。

2.1 async函数的定义

定义一个async函数非常简单,只需要在函数定义前加上async关键字:

async function myAsyncFunction() {
    return 'This is a result from async function';
}

这里,myAsyncFunction就是一个async函数。注意,async函数始终会返回一个Promise对象。如果函数的返回值不是Promise,JavaScript会自动将其包装成一个已解决状态(resolved)的Promise对象。例如:

async function returnString() {
    return 'Hello';
}
returnString().then((value) => console.log(value)); // 输出: Hello

上述代码中,returnString函数返回一个字符串,JavaScript自动将这个字符串包装成一个已解决状态的Promise对象,并且.then()方法可以获取到这个字符串值。

2.2 async函数中的Promise

async函数的返回值是一个Promise对象时,async函数的状态会与这个Promise对象的状态保持一致。例如:

function returnPromise() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Resolved after 1 second');
        }, 1000);
    });
}
async function asyncWithPromise() {
    return returnPromise();
}
asyncWithPromise().then((value) => console.log(value)); // 输出: Resolved after 1 second

在这个例子中,asyncWithPromise函数返回了returnPromise函数所返回的Promise对象。asyncWithPromise函数的状态会随着returnPromise返回的Promise对象的状态变化而变化,最终在Promise被解决(resolved)时,.then()方法获取到了Promise的解决值。

3. await关键字

await关键字只能在async函数内部使用,它用于暂停async函数的执行,直到Promise被解决(resolved)或被拒绝(rejected)。

3.1 await等待Promise解决

function returnPromise() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Resolved value');
        }, 1000);
    });
}
async function asyncFunctionWithAwait() {
    let result = await returnPromise();
    console.log(result); // 输出: Resolved value
}
asyncFunctionWithAwait();

在上述代码中,await returnPromise()会暂停asyncFunctionWithAwait函数的执行,直到returnPromise返回的Promise被解决。当Promise被解决后,await表达式的值就是Promise的解决值,这里赋值给result变量,然后输出结果。

3.2 await与多个Promise

当有多个Promise需要等待时,可以使用Promise.all结合awaitPromise.all接受一个Promise数组作为参数,并返回一个新的Promise,只有当所有传入的Promise都被解决时,这个新的Promise才会被解决。例如:

function promise1() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Promise 1 resolved');
        }, 1000);
    });
}
function promise2() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Promise 2 resolved');
        }, 1500);
    });
}
async function multiplePromises() {
    let [result1, result2] = await Promise.all([promise1(), promise2()]);
    console.log(result1); // 输出: Promise 1 resolved
    console.log(result2); // 输出: Promise 2 resolved
}
multiplePromises();

这里,Promise.allpromise1promise2两个Promise包装成一个新的Promise。await等待这个新的Promise被解决,当两个Promise都被解决后,await表达式的值是一个包含两个Promise解决值的数组,通过解构赋值分别赋给result1result2变量。

3.3 await处理Promise被拒绝的情况

await不仅可以处理Promise被解决的情况,也能处理Promise被拒绝的情况。当Promise被拒绝时,await会抛出一个错误,可以通过try...catch块来捕获这个错误。例如:

function rejectedPromise() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('Promise rejected');
        }, 1000);
    });
}
async function handleRejectedPromise() {
    try {
        let result = await rejectedPromise();
        console.log(result);
    } catch (error) {
        console.log(error); // 输出: Promise rejected
    }
}
handleRejectedPromise();

在这个例子中,rejectedPromise返回一个被拒绝的Promise。await捕获到这个拒绝,并将错误抛给try...catch块中的catch部分,从而输出错误信息。

4. async和await在实际场景中的应用

4.1 网络请求

在进行网络请求时,asyncawait可以让代码更加简洁。例如,使用fetch进行HTTP请求:

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

这里,首先await fetch('https://example.com/api/data')等待fetch操作完成,获取到响应。然后检查响应状态,如果状态不是ok,则抛出错误。接着await response.json()等待将响应数据解析为JSON格式,最后输出数据。如果在任何一步出现错误,都会被catch块捕获并处理。

4.2 文件读取

在Node.js环境中,读取文件也是一个异步操作,可以使用asyncawait来处理。假设使用fs/promises模块(Node.js 14+):

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

在这个例子中,await fs.readFile('example.txt', 'utf8')暂停函数执行,直到文件读取操作完成。如果读取成功,await表达式的值就是文件内容,赋值给content变量并输出。如果出现错误,catch块捕获并处理错误。

5. async和await的错误处理

async函数中,错误处理非常重要。除了前面提到的使用try...catch块捕获await操作抛出的错误外,async函数本身也可以像普通函数一样抛出错误。例如:

async function mightThrowError() {
    if (Math.random() > 0.5) {
        throw new Error('Random error');
    }
    return 'Success';
}
mightThrowError()
  .then((value) => console.log(value))
  .catch((error) => console.error(error));

在这个例子中,mightThrowError函数内部根据随机数决定是否抛出错误。如果抛出错误,.catch()方法会捕获到这个错误并进行处理。

另外,在多个await操作的async函数中,错误处理需要注意。例如:

async function multipleAwaits() {
    try {
        let result1 = await firstAsyncOperation();
        let result2 = await secondAsyncOperation(result1);
        let result3 = await thirdAsyncOperation(result2);
        console.log(result3);
    } catch (error) {
        console.error('Error in async operations:', error);
    }
}

在这个函数中,如果firstAsyncOperation抛出错误,await会捕获并将错误传递给catch块,后续的secondAsyncOperationthirdAsyncOperation不会执行。这确保了在异步操作链中,一旦某个环节出现错误,整个流程能够及时停止并进行错误处理。

6. async和await与同步代码的混合

虽然async函数主要用于处理异步操作,但它也可以与同步代码混合使用。例如:

async function mixSyncAndAsync() {
    console.log('This is a synchronous log');
    let result = await asyncOperation();
    console.log('This is another synchronous log after async operation:', result);
}
function asyncOperation() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Async result');
        }, 1000);
    });
}
mixSyncAndAsync();

在这个例子中,async函数开始时输出了一条同步日志,然后等待asyncOperation这个异步操作完成,获取结果后又输出了一条同步日志。这展示了async函数可以自然地将同步代码和异步代码结合在一起,提高代码的灵活性。

7. 性能方面的考虑

从性能角度来看,asyncawait本身并不会提升异步操作的执行速度。它们主要的作用是让异步代码的编写和阅读更加容易。然而,合理使用asyncawait可以间接地提高性能。

例如,在处理多个异步任务时,通过Promise.all结合await可以并行执行多个Promise,减少总体执行时间。对比串行执行多个异步任务:

// 串行执行
async function serialTasks() {
    let result1 = await task1();
    let result2 = await task2(result1);
    let result3 = await task3(result2);
    return result3;
}
// 并行执行
async function parallelTasks() {
    let [result1, result2, result3] = await Promise.all([task1(), task2(), task3()]);
    return result3;
}

在这个例子中,如果task1task2task3之间没有依赖关系,并行执行的parallelTasks函数会比串行执行的serialTasks函数更快完成,因为多个任务可以同时进行,而不是一个接一个地等待前一个任务完成。

8. 与其他异步编程模式的对比

8.1 与回调函数的对比

回调函数是早期JavaScript处理异步操作的主要方式,但正如前面提到的,回调地狱使得代码难以维护。asyncawait通过将异步代码写得像同步代码,极大地改善了代码的可读性和可维护性。例如,对比以下两种方式处理文件读取: 回调函数方式(使用fs模块旧的回调风格)

const fs = require('fs');
fs.readFile('example.txt', 'utf8', (err, data) => {
    if (err) {
        console.error(err);
    } else {
        console.log(data);
    }
});

async和await方式(使用fs/promises模块)

const fs = require('fs/promises');
async function readFileWithAsyncAwait() {
    try {
        let data = await fs.readFile('example.txt', 'utf8');
        console.log(data);
    } catch (err) {
        console.error(err);
    }
}
readFileWithAsyncAwait();

可以明显看出,asyncawait方式的代码结构更加清晰,错误处理也更加直观。

8.2 与Promise链式调用的对比

Promise链式调用解决了回调地狱的问题,但对于复杂的异步流程,代码依然不够简洁。asyncawait在Promise的基础上,进一步简化了异步代码的书写。例如,对比以下两种方式进行多个异步操作: Promise链式调用方式

function task1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Task 1 result');
        }, 1000);
    });
}
function task2(result1) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(result1 +'-> Task 2 result');
        }, 1000);
    });
}
function task3(result2) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(result2 +'-> Task 3 result');
        }, 1000);
    });
}
task1()
  .then(task2)
  .then(task3)
  .then((finalResult) => console.log(finalResult));

async和await方式

function task1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Task 1 result');
        }, 1000);
    });
}
function task2(result1) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(result1 +'-> Task 2 result');
        }, 1000);
    });
}
function task3(result2) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(result2 +'-> Task 3 result');
        }, 1000);
    });
}
async function combinedTasks() {
    let result1 = await task1();
    let result2 = await task2(result1);
    let result3 = await task3(result2);
    console.log(result3);
}
combinedTasks();

asyncawait方式的代码更接近同步代码的书写习惯,更易于理解和维护。

9. 浏览器和Node.js兼容性

asyncawait是ES2017的特性,现代浏览器和Node.js版本都对其提供了良好的支持。然而,在一些旧版本的浏览器或Node.js环境中,可能需要使用转译工具(如Babel)来将包含asyncawait的代码转换为ES5兼容的代码。

在Node.js环境中,如果版本较低,可以通过安装Babel并配置转译来使用asyncawait。例如,在项目中安装@babel/core@babel/cli,并创建一个.babelrc配置文件:

{
    "presets": ["@babel/preset - env"]
}

然后可以通过命令npx babel src - d distsrc目录下的代码转译到dist目录,使其能够在旧版本Node.js环境中运行。

在浏览器环境中,同样可以使用Babel进行转译,也可以使用Webpack等构建工具集成Babel转译功能,确保代码在各种浏览器中都能正常运行。

10. 总结常见问题及解决方案

在使用asyncawait过程中,可能会遇到一些常见问题:

10.1 await在非async函数中使用

如果在非async函数中使用await,会导致语法错误。例如:

function nonAsyncFunction() {
    await Promise.resolve('This will cause an error'); // 语法错误
}

解决方案就是将函数定义为async函数:

async function asyncFunction() {
    let result = await Promise.resolve('This works');
    console.log(result);
}
asyncFunction();

10.2 忘记处理Promise被拒绝的情况

如果在async函数中使用await但没有处理Promise被拒绝的情况,错误会导致程序崩溃。例如:

async function forgetRejection() {
    let result = await Promise.reject('Error');
    console.log(result); // 这行代码不会执行,错误会导致程序崩溃
}
forgetRejection();

解决方案是使用try...catch块来捕获错误:

async function handleRejection() {
    try {
        let result = await Promise.reject('Error');
        console.log(result);
    } catch (error) {
        console.error('Caught error:', error);
    }
}
handleRejection();

10.3 性能问题

虽然asyncawait本身不提升性能,但不合理的使用可能导致性能问题。例如,不必要的串行执行异步任务。解决方案是分析异步任务之间的依赖关系,尽量使用Promise.all进行并行执行,提高执行效率。

通过深入理解asyncawait的基本用法、错误处理、与其他异步编程模式的对比以及兼容性等方面,开发者可以在JavaScript项目中更加高效地处理异步操作,编写出更简洁、易维护且性能良好的代码。无论是在前端开发还是后端开发中,asyncawait都已经成为处理异步任务的重要工具。