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

Node.js异步迭代器async/await用法详解

2022-07-244.6k 阅读

异步编程在 Node.js 中的重要性

在 Node.js 开发领域,异步编程占据着核心地位。Node.js 之所以能够在高并发场景下表现出色,很大程度上得益于其对异步操作的良好支持。传统的同步编程模式在处理 I/O 密集型任务(如文件读取、网络请求等)时,会阻塞主线程,导致程序无法继续执行其他任务,直到当前操作完成。这在现代应用程序中是不可接受的,因为用户希望应用能够快速响应,即使在处理复杂任务时也不卡顿。

而异步编程则允许 Node.js 在执行 I/O 操作时,不阻塞主线程,而是继续执行后续代码。当 I/O 操作完成后,通过回调函数、Promise 或者更先进的 async/await 机制来处理结果。这种方式大大提高了程序的性能和响应能力,使得 Node.js 成为构建高性能网络应用、实时应用(如 WebSocket 服务)以及微服务架构的首选技术之一。

迭代器与异步迭代器基础

迭代器概念

迭代器(Iterator)是一种设计模式,它提供了一种顺序访问一个聚合对象中各个元素的方法,而又不暴露该对象的内部表示。在 JavaScript 中,迭代器是一个对象,它实现了 next() 方法。每次调用 next() 方法时,迭代器会返回一个包含 valuedone 两个属性的对象。value 表示当前迭代的值,done 是一个布尔值,当迭代结束时为 true,否则为 false

// 创建一个简单的迭代器
function createIterator() {
    let index = 0;
    const data = [1, 2, 3];
    return {
        next: function () {
            if (index < data.length) {
                return { value: data[index++], done: false };
            } else {
                return { value: undefined, done: true };
            }
        }
    };
}

const iterator = createIterator();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

可迭代对象

可迭代对象(Iterable)是指实现了 Symbol.iterator 方法的对象。该方法返回一个迭代器对象。JavaScript 中有许多内置的可迭代对象,如数组、字符串、Map 和 Set 等。

const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

异步迭代器

异步迭代器(Async Iterator)是迭代器概念的异步版本。它同样实现了 next() 方法,但这个方法返回的是一个 Promise 对象。每次调用 next() 方法后,通过 await 关键字等待 Promise 解决,从而获取 valuedone 属性。

async function asyncGenerator() {
    yield 1;
    yield 2;
    yield 3;
}

const asyncIterator = asyncGenerator()[Symbol.asyncIterator]();
async function consumeAsyncIterator() {
    let result = await asyncIterator.next();
    while (!result.done) {
        console.log(result.value);
        result = await asyncIterator.next();
    }
}

consumeAsyncIterator();
// 输出:
// 1
// 2
// 3

async/await 语法基础

async 函数定义

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

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

asyncFunction().then(value => {
    console.log(value); // 输出: Hello, async!
});

await 关键字

await 关键字只能在 async 函数内部使用。它用于暂停 async 函数的执行,直到其等待的 Promise 被解决(resolved)或被拒绝(rejected)。当 Promise 被解决时,await 返回 Promise 的解决值;当 Promise 被拒绝时,await 会抛出异常。

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

asyncFunction();

async/await 与异步迭代器结合使用

异步生成器函数

异步生成器函数(Async Generator Function)是一种特殊的异步函数,它返回一个异步迭代器。通过 yield 关键字可以暂停函数执行,并返回一个值,同时将控制权交还给调用者。与普通生成器函数不同的是,异步生成器函数中的 yield 可以返回一个 Promise 对象,通过 await 来处理。

async function* asyncGeneratorFunction() {
    yield await new Promise((resolve) => {
        setTimeout(() => {
            resolve(1);
        }, 1000);
    });
    yield await new Promise((resolve) => {
        setTimeout(() => {
            resolve(2);
        }, 1000);
    });
    yield await new Promise((resolve) => {
        setTimeout(() => {
            resolve(3);
        }, 1000);
    });
}

const asyncIterator = asyncGeneratorFunction();
async function consumeAsyncIterator() {
    let result = await asyncIterator.next();
    while (!result.done) {
        console.log(result.value);
        result = await asyncIterator.next();
    }
}

consumeAsyncIterator();
// 输出:
// 1 (1秒后)
// 2 (再过1秒后)
// 3 (再过1秒后)

for - await...of 循环

for - await...of 循环是专门用于遍历异步迭代器的语法糖。它会自动等待每个 await 操作,使得代码更加简洁易读。

async function* asyncGeneratorFunction() {
    yield await new Promise((resolve) => {
        setTimeout(() => {
            resolve(1);
        }, 1000);
    });
    yield await new Promise((resolve) => {
        setTimeout(() => {
            resolve(2);
        }, 1000);
    });
    yield await new Promise((resolve) => {
        setTimeout(() => {
            resolve(3);
        }, 1000);
    });
}

async function consumeWithForAwaitOf() {
    for await (const value of asyncGeneratorFunction()) {
        console.log(value);
    }
}

consumeWithForAwaitOf();
// 输出:
// 1 (1秒后)
// 2 (再过1秒后)
// 3 (再过1秒后)

实际应用场景中的 async/await 与异步迭代器

文件系统操作

在 Node.js 中,文件系统(fs)模块的许多操作都是异步的。使用异步迭代器和 async/await 可以方便地处理多个文件的读取或写入操作。

const fs = require('fs').promises;
const path = require('path');

async function* readFilesInDirectory(directory) {
    const files = await fs.readdir(directory);
    for (const file of files) {
        const filePath = path.join(directory, file);
        const stats = await fs.stat(filePath);
        if (stats.isFile()) {
            const content = await fs.readFile(filePath, 'utf8');
            yield content;
        }
    }
}

async function processFiles() {
    for await (const content of readFilesInDirectory('./')) {
        console.log(content);
    }
}

processFiles();

网络请求

在处理多个网络请求时,异步迭代器和 async/await 可以帮助我们更优雅地管理请求流程。例如,使用 axios 库进行多个 API 请求。

const axios = require('axios');

async function* fetchAPIData() {
    const urls = ['https://example.com/api1', 'https://example.com/api2', 'https://example.com/api3'];
    for (const url of urls) {
        const response = await axios.get(url);
        yield response.data;
    }
}

async function processAPIData() {
    for await (const data of fetchAPIData()) {
        console.log(data);
    }
}

processAPIData();

数据库操作

在数据库操作中,比如使用 mongoose 操作 MongoDB,异步迭代器和 async/await 可以简化对多个文档的查询、更新等操作。

const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/mydb', { useNewUrlParser: true, useUnifiedTopology: true });

const User = mongoose.model('User', new mongoose.Schema({ name: String, age: Number }));

async function* findAllUsers() {
    const users = await User.find();
    for (const user of users) {
        yield user;
    }
}

async function processUsers() {
    for await (const user of findAllUsers()) {
        console.log(user.name, user.age);
    }
}

processUsers();

错误处理与异步迭代器和 async/await

try...catch 捕获错误

在使用 async/await 和异步迭代器时,错误处理非常重要。通过 try...catch 块可以捕获 await 操作中 Promise 被拒绝时抛出的异常。

async function asyncFunction() {
    try {
        const result = await new Promise((_, reject) => {
            setTimeout(() => {
                reject(new Error('Promise rejected'));
            }, 1000);
        });
        console.log(result);
    } catch (error) {
        console.error('Error caught:', error.message);
    }
}

asyncFunction();
// 输出: Error caught: Promise rejected

在异步迭代器中使用 for - await...of 循环时,也可以通过 try...catch 块来捕获错误。

async function* asyncGeneratorWithError() {
    yield 1;
    throw new Error('Generator error');
    yield 2;
}

async function consumeWithErrorHandling() {
    try {
        for await (const value of asyncGeneratorWithError()) {
            console.log(value);
        }
    } catch (error) {
        console.error('Error in for - await...of:', error.message);
    }
}

consumeWithErrorHandling();
// 输出:
// 1
// Error in for - await...of: Generator error

错误处理策略

在实际应用中,需要根据不同的场景制定合适的错误处理策略。对于一些临时的网络错误,可以选择重试机制。例如,在网络请求失败时,进行多次重试。

const axios = require('axios');

async function retryRequest(url, maxRetries = 3, delay = 1000) {
    let retries = 0;
    while (retries < maxRetries) {
        try {
            const response = await axios.get(url);
            return response.data;
        } catch (error) {
            retries++;
            if (retries === maxRetries) {
                throw error;
            }
            await new Promise((resolve) => {
                setTimeout(resolve, delay);
            });
        }
    }
}

async function processRequest() {
    try {
        const data = await retryRequest('https://example.com/api', 5, 2000);
        console.log(data);
    } catch (error) {
        console.error('Final error:', error.message);
    }
}

processRequest();

性能优化与异步迭代器和 async/await

并发与串行执行

在处理多个异步任务时,需要根据具体情况选择并发执行还是串行执行。并发执行可以利用多核 CPU 的优势,加快整体任务的完成速度,但可能会消耗更多的系统资源。串行执行则可以避免资源竞争问题,但执行时间可能会更长。

使用 Promise.all 可以实现并发执行多个异步任务。

const axios = require('axios');

async function concurrentRequests() {
    const urls = ['https://example.com/api1', 'https://example.com/api2', 'https://example.com/api3'];
    const promises = urls.map(url => axios.get(url));
    const results = await Promise.all(promises);
    results.forEach((result, index) => {
        console.log(`Result from ${urls[index]}:`, result.data);
    });
}

concurrentRequests();

而使用异步迭代器和 for - await...of 循环则实现了串行执行。

const axios = require('axios');

async function* sequentialRequests() {
    const urls = ['https://example.com/api1', 'https://example.com/api2', 'https://example.com/api3'];
    for (const url of urls) {
        const response = await axios.get(url);
        yield response.data;
    }
}

async function processSequentialRequests() {
    for await (const data of sequentialRequests()) {
        console.log(data);
    }
}

processSequentialRequests();

资源管理

在异步迭代器中,合理管理资源非常重要。例如,在处理文件系统操作时,要确保文件在使用完毕后及时关闭,避免资源泄漏。

const fs = require('fs').promises;
const path = require('path');

async function* readFilesInDirectory(directory) {
    const files = await fs.readdir(directory);
    for (const file of files) {
        const filePath = path.join(directory, file);
        const stats = await fs.stat(filePath);
        if (stats.isFile()) {
            const fileHandle = await fs.open(filePath, 'r');
            try {
                const content = await fileHandle.readFile('utf8');
                yield content;
            } finally {
                await fileHandle.close();
            }
        }
    }
}

async function processFiles() {
    for await (const content of readFilesInDirectory('./')) {
        console.log(content);
    }
}

processFiles();

与其他异步编程模式的比较

与回调函数对比

回调函数是 JavaScript 早期处理异步操作的主要方式。虽然它简单直接,但当异步操作嵌套过多时,会导致代码可读性和维护性变差,形成所谓的“回调地狱”。

// 回调函数示例
const fs = require('fs');

fs.readFile('file1.txt', 'utf8', (err, data1) => {
    if (err) {
        console.error(err);
        return;
    }
    fs.readFile('file2.txt', 'utf8', (err, data2) => {
        if (err) {
            console.error(err);
            return;
        }
        fs.readFile('file3.txt', 'utf8', (err, data3) => {
            if (err) {
                console.error(err);
                return;
            }
            console.log(data1, data2, data3);
        });
    });
});

而使用 async/await 可以使代码结构更加清晰,类似于同步代码的书写方式。

const fs = require('fs').promises;

async function readFiles() {
    try {
        const data1 = await fs.readFile('file1.txt', 'utf8');
        const data2 = await fs.readFile('file2.txt', 'utf8');
        const data3 = await fs.readFile('file3.txt', 'utf8');
        console.log(data1, data2, data3);
    } catch (error) {
        console.error(error);
    }
}

readFiles();

与 Promise 对比

Promise 解决了回调地狱的问题,通过链式调用的方式处理异步操作。但在处理复杂的异步流程时,async/await 基于 Promise 构建,语法上更加简洁,并且错误处理更加直观。

// Promise 链式调用示例
const fs = require('fs').promises;

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

async/await 代码更加简洁明了。

const fs = require('fs').promises;

async function readFiles() {
    try {
        const data1 = await fs.readFile('file1.txt', 'utf8');
        const data2 = await fs.readFile('file2.txt', 'utf8');
        const data3 = await fs.readFile('file3.txt', 'utf8');
        console.log(data1, data2, data3);
    } catch (error) {
        console.error(error);
    }
}

readFiles();

总结 async/await 与异步迭代器的优势

async/await 与异步迭代器在 Node.js 异步编程中具有诸多优势。它们使异步代码的书写更加接近同步代码,大大提高了代码的可读性和可维护性。通过简洁的语法,能够方便地处理复杂的异步流程,无论是文件系统操作、网络请求还是数据库交互。同时,与传统的回调函数和 Promise 相比,错误处理更加直观和统一。在性能优化方面,通过合理选择并发或串行执行方式,以及有效的资源管理,能够提升应用程序的整体性能。因此,熟练掌握 async/await 与异步迭代器的用法,对于 Node.js 开发者来说至关重要,能够帮助开发者构建出高效、稳定且易于维护的应用程序。

未来发展趋势与拓展

随着 JavaScript 语言的不断发展,异步编程模式也在持续演进。async/await 与异步迭代器作为当前主流的异步编程方式,未来有望在更多的 JavaScript 运行时环境中得到进一步优化和拓展。例如,在即将到来的 ECMAScript 规范更新中,可能会针对异步迭代器增加更多便捷的方法和特性,使得异步数据处理更加灵活高效。同时,随着硬件性能的提升和应用场景的日益复杂,如何更好地利用多核 CPU 资源,进一步优化异步任务的并发执行策略,也将是未来研究和发展的方向。在框架层面,像 React、Vue 等前端框架以及 Express、Koa 等 Node.js 后端框架,也会越来越多地基于 async/await 来构建更高效的异步数据流处理机制,为开发者提供更加简洁易用的异步编程体验。总之,async/await 与异步迭代器作为现代 JavaScript 异步编程的核心,将在未来的技术发展中扮演更加重要的角色。