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

使用async函数优化TypeScript异步代码

2022-06-154.3k 阅读

一、JavaScript 异步编程的发展历程

在深入探讨 TypeScript 中 async 函数对异步代码的优化之前,让我们先回顾一下 JavaScript 异步编程的发展历程。JavaScript 作为一门单线程语言,为了处理 I/O 操作(如网络请求、文件读取等)而不阻塞主线程,异步编程显得尤为重要。

1.1 回调函数(Callbacks)

JavaScript 异步编程最开始使用的是回调函数。例如,在 Node.js 中读取文件的操作:

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

在这个例子中,readFile 是一个异步操作,它接受文件名、编码格式以及一个回调函数作为参数。当文件读取完成后,会调用这个回调函数,并将可能的错误 err 和读取到的数据 data 作为参数传递进去。

然而,回调函数存在一些问题,其中最典型的就是 “回调地狱”。当有多个异步操作需要依次执行,并且每个操作都依赖上一个操作的结果时,代码会变得非常难以阅读和维护。例如:

fs.readFile('file1.txt', 'utf8', function (err1, data1) {
    if (err1) {
        console.error(err1);
        return;
    }
    fs.readFile('file2.txt', 'utf8', function (err2, data2) {
        if (err2) {
            console.error(err2);
            return;
        }
        fs.readFile('file3.txt', 'utf8', function (err3, data3) {
            if (err3) {
                console.error(err3);
                return;
            }
            console.log(data1 + data2 + data3);
        });
    });
});

这种层层嵌套的代码结构不仅难以理解,而且一旦出现错误,定位和调试都非常困难。

1.2 Promise

为了解决回调地狱的问题,Promise 应运而生。Promise 是一个表示异步操作最终完成(或失败)及其结果值的对象。使用 Promise 改写上面读取文件的例子:

const fs = require('fs');
const { promisify } = require('util');

const readFileAsync = promisify(fs.readFile);

readFileAsync('example.txt', 'utf8')
   .then(data => {
        console.log(data);
    })
   .catch(err => {
        console.error(err);
    });

这里通过 util.promisifyfs.readFile 这个基于回调的函数转换为返回 Promise 的函数。then 方法用于处理 Promise 成功的情况,catch 方法用于捕获 Promise 中的错误。

当有多个异步操作依次执行时,Promise 的链式调用使得代码结构更加清晰:

readFileAsync('file1.txt', 'utf8')
   .then(data1 => {
        return readFileAsync('file2.txt', 'utf8').then(data2 => {
            return readFileAsync('file3.txt', 'utf8').then(data3 => {
                return data1 + data2 + data3;
            });
        });
    })
   .then(result => {
        console.log(result);
    })
   .catch(err => {
        console.error(err);
    });

虽然 Promise 解决了回调地狱的问题,但链式调用仍然显得有些繁琐,尤其是当异步操作较多时。

二、TypeScript 对异步编程的支持

TypeScript 是 JavaScript 的超集,它在 JavaScript 的基础上增加了类型系统。对于异步编程,TypeScript 同样支持回调函数、Promise 等方式,并且在类型检查方面提供了更强大的功能。

2.1 使用 Promise 进行异步操作

在 TypeScript 中使用 Promise 和 JavaScript 类似,但可以通过类型注解使代码更加严谨。例如,定义一个返回 Promise 的函数:

function fetchData(): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Data fetched successfully');
        }, 1000);
    });
}

fetchData()
   .then(data => {
        console.log(data);
    })
   .catch(err => {
        console.error(err);
    });

这里 fetchData 函数明确声明返回一个 Promise<string>,表示这个 Promise 成功时会返回一个字符串类型的数据。

2.2 处理异步函数的类型

当函数内部包含异步操作时,我们可以通过 asyncawait 来处理,并且 TypeScript 能够很好地推断出返回值的类型。例如:

async function getData(): Promise<string> {
    const response = await fetch('https://example.com/api/data');
    const data = await response.json();
    return data.message;
}

getData()
   .then(message => {
        console.log(message);
    })
   .catch(err => {
        console.error(err);
    });

在这个例子中,getData 函数是一个 async 函数,它返回一个 Promise<string>await 只能在 async 函数内部使用,用于等待一个 Promise 解决,并返回其解决的值。

三、async 函数的基本概念

async 函数是 ES2017 引入的异步函数语法,它是基于 Promise 的一种更简洁的异步编程方式。在 TypeScript 中,async 函数同样具有重要的作用。

3.1 定义 async 函数

async 函数的定义非常简单,只需在函数声明前加上 async 关键字。例如:

async function asyncFunction() {
    return 'Hello, async!';
}

这里 asyncFunction 是一个 async 函数,它返回一个 Promise。即使函数内部没有显式地返回 Promise,async 函数也会自动将返回值包装成一个已解决的 Promise。例如,上面的函数等价于:

function asyncFunction(): Promise<string> {
    return Promise.resolve('Hello, async!');
}

3.2 async 函数的返回值

async 函数的返回值始终是一个 Promise。如果 async 函数返回一个非 Promise 值,它会被自动包装成一个已解决的 Promise。例如:

async function returnNumber(): number {
    return 42;
}

returnNumber().then(value => {
    console.log(value); // 输出 42
});

这里 returnNumber 函数返回一个数字 42,但实际上它返回的是 Promise.resolve(42)

如果 async 函数抛出一个错误,它会返回一个被拒绝的 Promise。例如:

async function throwError() {
    throw new Error('Something went wrong');
}

throwError().catch(err => {
    console.error(err.message); // 输出 'Something went wrong'
});

四、await 关键字的使用

await 关键字只能在 async 函数内部使用,它用于暂停 async 函数的执行,等待一个 Promise 解决,并返回其解决的值。

4.1 等待 Promise 解决

假设我们有一个返回 Promise 的函数 fetchData,可以在 async 函数中使用 await 等待它的结果:

function fetchData(): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Data fetched');
        }, 1000);
    });
}

async function processData() {
    const data = await fetchData();
    console.log(data); // 输出 'Data fetched'
}

processData();

processData 函数中,await fetchData() 会暂停函数的执行,直到 fetchData 返回的 Promise 被解决。一旦 Promise 被解决,await 表达式会返回 Promise 的解决值,这里就是字符串 'Data fetched'

4.2 处理多个异步操作

await 使得处理多个异步操作变得非常简单。例如,假设有两个异步函数 fetchFirstDatafetchSecondData,并且第二个操作依赖第一个操作的结果:

function fetchFirstData(): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('First data');
        }, 1000);
    });
}

function fetchSecondData(data: string): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(data +'and second data');
        }, 1000);
    });
}

async function processData() {
    const firstData = await fetchFirstData();
    const secondData = await fetchSecondData(firstData);
    console.log(secondData); // 输出 'First data and second data'
}

processData();

在这个例子中,先等待 fetchFirstData 完成并获取其结果,然后将这个结果作为参数传递给 fetchSecondData,再等待 fetchSecondData 完成。整个过程通过 await 清晰地表达了异步操作的顺序。

五、使用 async 函数优化异步代码结构

5.1 解决 Promise 链式调用的繁琐问题

在前面提到过,Promise 的链式调用在处理多个异步操作时会显得繁琐。使用 async 函数和 await 可以使代码结构更加简洁和易读。对比以下两种方式: 使用 Promise 链式调用:

function step1(): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Step 1 result');
        }, 1000);
    });
}

function step2(data: string): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(data +'and Step 2 result');
        }, 1000);
    });
}

function step3(data: string): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(data +'and Step 3 result');
        }, 1000);
    });
}

step1()
   .then(result1 => {
        return step2(result1);
    })
   .then(result2 => {
        return step3(result2);
    })
   .then(finalResult => {
        console.log(finalResult);
    })
   .catch(err => {
        console.error(err);
    });

使用 async 函数和 await:

async function processSteps() {
    const result1 = await step1();
    const result2 = await step2(result1);
    const finalResult = await step3(result2);
    console.log(finalResult);
}

processSteps().catch(err => {
    console.error(err);
});

可以明显看出,使用 async 函数和 await 后,代码的结构更像是同步代码,更易于理解和维护。

5.2 错误处理的优化

async 函数中,错误处理也变得更加直观。async 函数内部抛出的错误或者 await 的 Promise 被拒绝时,会被 async 函数返回的 Promise 捕获。可以使用 try...catch 块来处理这些错误。例如:

function fetchData(): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject(new Error('Fetch error'));
        }, 1000);
    });
}

async function processData() {
    try {
        const data = await fetchData();
        console.log(data);
    } catch (err) {
        console.error(err.message); // 输出 'Fetch error'
    }
}

processData();

这种方式相比于 Promise 的 catch 链式调用,在处理复杂的异步操作和错误时更加清晰。例如,在多个异步操作的情况下:

async function complexProcess() {
    try {
        const result1 = await step1();
        const result2 = await step2(result1);
        const finalResult = await step3(result2);
        console.log(finalResult);
    } catch (err) {
        console.error(err.message);
    }
}

这里所有的异步操作都在一个 try...catch 块中,一旦某个异步操作出错,就会被统一捕获和处理。

六、并发和并行异步操作

在实际应用中,有时我们需要同时执行多个异步操作,而不是依次执行。async 函数和 Promise 提供了一些方法来实现并发和并行异步操作。

6.1 使用 Promise.all 实现并发操作

Promise.all 方法接受一个 Promise 数组作为参数,并返回一个新的 Promise。这个新的 Promise 在所有输入的 Promise 都被解决时才会被解决,并且它的解决值是一个包含所有输入 Promise 解决值的数组。例如:

function fetchData1(): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Data 1');
        }, 1000);
    });
}

function fetchData2(): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Data 2');
        }, 1500);
    });
}

async function processData() {
    const [data1, data2] = await Promise.all([fetchData1(), fetchData2()]);
    console.log(data1 +'and'+ data2);
}

processData();

在这个例子中,fetchData1fetchData2 会同时开始执行,Promise.all 会等待它们都完成,然后将它们的结果按顺序放入数组中,通过解构赋值可以方便地获取每个结果。

如果其中任何一个 Promise 被拒绝,Promise.all 返回的 Promise 也会被拒绝。例如:

function fetchData1(): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject(new Error('Error in fetchData1'));
        }, 1000);
    });
}

function fetchData2(): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Data 2');
        }, 1500);
    });
}

async function processData() {
    try {
        const [data1, data2] = await Promise.all([fetchData1(), fetchData2()]);
        console.log(data1 +'and'+ data2);
    } catch (err) {
        console.error(err.message); // 输出 'Error in fetchData1'
    }
}

processData();

6.2 使用 Promise.race 实现并行操作

Promise.race 方法同样接受一个 Promise 数组作为参数,并返回一个新的 Promise。但这个新的 Promise 会在数组中第一个被解决(或被拒绝)的 Promise 完成时就完成,它的解决值(或拒绝原因)就是第一个完成的 Promise 的解决值(或拒绝原因)。例如:

function fetchData1(): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Data 1');
        }, 1000);
    });
}

function fetchData2(): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Data 2');
        }, 1500);
    });
}

async function processData() {
    const result = await Promise.race([fetchData1(), fetchData2()]);
    console.log(result); // 输出 'Data 1'
}

processData();

在这个例子中,fetchData1 会先完成,所以 Promise.race 返回的 Promise 会以 fetchData1 的结果被解决。

七、在实际项目中应用 async 函数优化异步代码

7.1 网络请求场景

在前端开发中,经常需要进行网络请求。使用 async 函数可以使网络请求的代码更加简洁和易于维护。例如,使用 fetch API 进行多个 API 请求:

async function getUserData() {
    const userResponse = await fetch('https://example.com/api/user');
    const userData = await userResponse.json();

    const orderResponse = await fetch(`https://example.com/api/orders/${userData.id}`);
    const orderData = await orderResponse.json();

    return { user: userData, orders: orderData };
}

getUserData().then(result => {
    console.log(result);
}).catch(err => {
    console.error(err);
});

这里通过 await 依次等待用户数据请求和订单数据请求的完成,并且将两个请求的结果组合在一起返回。

7.2 数据库操作场景

在后端开发中,数据库操作通常也是异步的。假设使用 Node.js 和 MongoDB,使用 async 函数可以优化数据库查询的代码。例如:

const { MongoClient } = require('mongodb');

async function getDocuments() {
    const uri = "mongodb://localhost:27017";
    const client = new MongoClient(uri);

    try {
        await client.connect();
        const database = client.db('test');
        const collection = database.collection('documents');

        const documents = await collection.find({}).toArray();
        return documents;
    } finally {
        await client.close();
    }
}

getDocuments().then(documents => {
    console.log(documents);
}).catch(err => {
    console.error(err);
});

在这个例子中,async 函数使得连接数据库、查询文档以及关闭连接的操作更加清晰和有序。

八、async 函数与其他异步编程模式的结合

8.1 async 函数与回调函数的结合

虽然 async 函数和 Promise 已经成为现代异步编程的主流,但在一些遗留代码中可能还会存在回调函数。可以将回调函数转换为 Promise,然后在 async 函数中使用。例如,假设我们有一个基于回调的函数 readFile

const fs = require('fs');
const { promisify } = require('util');

const readFileAsync = promisify(fs.readFile);

async function readFiles() {
    try {
        const data1 = await readFileAsync('file1.txt', 'utf8');
        const data2 = await readFileAsync('file2.txt', 'utf8');
        console.log(data1 + data2);
    } catch (err) {
        console.error(err);
    }
}

readFiles();

这里通过 promisify 将基于回调的 fs.readFile 转换为返回 Promise 的函数,然后在 async 函数中使用。

8.2 async 函数与事件驱动编程的结合

在一些场景下,可能需要将 async 函数与事件驱动编程结合。例如,在 Node.js 中处理 HTTP 服务器的请求:

const http = require('http');
const { promisify } = require('util');
const fs = require('fs');

const readFileAsync = promisify(fs.readFile);

async function handleRequest(req, res) {
    try {
        const data = await readFileAsync('index.html', 'utf8');
        res.writeHead(200, { 'Content-Type': 'text/html' });
        res.end(data);
    } catch (err) {
        res.writeHead(500, { 'Content-Type': 'text/plain' });
        res.end('Error:'+ err.message);
    }
}

const server = http.createServer(handleRequest);
server.listen(3000, () => {
    console.log('Server running on port 3000');
});

在这个例子中,handleRequest 是一个 async 函数,用于处理 HTTP 请求。它读取 HTML 文件并返回给客户端,如果发生错误则返回错误信息。

九、注意事项和常见问题

9.1 await 只能在 async 函数内部使用

这是一个常见的错误,如果在普通函数中使用 await,会导致语法错误。例如:

function regularFunction() {
    const result = await fetchData(); // 报错:await 仅在 async 函数内有效
    console.log(result);
}

正确的做法是将这个函数定义为 async 函数:

async function asyncFunction() {
    const result = await fetchData();
    console.log(result);
}

9.2 处理未捕获的 Promise 错误

虽然 async 函数内部可以通过 try...catch 捕获错误,但如果在 async 函数外部没有处理返回的 Promise 被拒绝的情况,可能会导致未捕获的 Promise 错误。例如:

async function asyncFunction() {
    throw new Error('Error in async function');
}

asyncFunction(); // 这里没有捕获错误,会导致未捕获的 Promise 错误

应该始终确保对 async 函数返回的 Promise 进行错误处理:

asyncFunction().catch(err => {
    console.error(err.message);
});

9.3 性能问题

虽然 async 函数和 await 使代码更易读,但在某些情况下可能会影响性能。例如,在处理大量并发异步操作时,过度使用 await 可能会导致性能瓶颈。在这种情况下,需要合理使用 Promise.all 等方法来优化性能。例如,不要在一个循环中依次使用 await 执行大量异步操作:

async function badPerformance() {
    const promises = [];
    for (let i = 0; i < 1000; i++) {
        promises.push(fetchData());
    }
    for (let i = 0; i < 1000; i++) {
        const result = await promises[i];
        console.log(result);
    }
}

更好的做法是使用 Promise.all 一次性处理所有 Promise:

async function betterPerformance() {
    const promises = [];
    for (let i = 0; i < 1000; i++) {
        promises.push(fetchData());
    }
    const results = await Promise.all(promises);
    results.forEach(result => {
        console.log(result);
    });
}

这样可以提高性能,因为所有异步操作可以并发执行,而不是依次等待每个操作完成。

通过以上对 async 函数在 TypeScript 异步代码优化中的深入探讨,我们可以看到 async 函数为异步编程带来了极大的便利,它使代码结构更清晰、错误处理更直观,并且能够与其他异步编程模式很好地结合。在实际项目中,合理运用 async 函数可以提高代码的可维护性和性能。