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

Node.js Promise 的基本概念与使用

2021-01-126.3k 阅读

什么是 Promise

在深入探讨 Node.js 中 Promise 的使用之前,我们首先要明白 Promise 是什么。简单来说,Promise 是 JavaScript 中用于处理异步操作的一种机制。在传统的 JavaScript 编程中,异步操作(如读取文件、发起网络请求等)通常通过回调函数来处理。然而,随着异步操作的复杂性增加,回调函数嵌套回调函数的方式会导致代码变得难以阅读和维护,这就是所谓的 “回调地狱”。

Promise 提供了一种更优雅、更结构化的方式来处理异步操作。它代表了一个尚未完成但预计将来会完成的操作,并最终会返回一个值或者抛出一个错误。一个 Promise 有三种状态:

  1. Pending(进行中):初始状态,既没有被兑现(resolved),也没有被拒绝(rejected)。
  2. Fulfilled(已兑现):意味着操作成功完成,Promise 会有一个 resolved 值。
  3. Rejected(已拒绝):意味着操作失败,Promise 会有一个 rejection 原因。

一旦 Promise 从 pending 状态转变为 fulfilled 或者 rejected 状态,它就被称为 “已解决(settled)”。并且,一旦 Promise 被解决,它的状态就不能再改变。

Promise 的基本语法

在 JavaScript 中,创建一个 Promise 对象很简单,通过 new Promise() 构造函数来创建。Promise 构造函数接受一个执行器函数作为参数,这个执行器函数会立即被执行。执行器函数接受两个参数:resolverejectresolve 函数用于将 Promise 的状态从 pending 转变为 fulfilled,并传递一个成功的值。reject 函数则用于将 Promise 的状态从 pending 转变为 rejected,并传递一个失败的原因。

以下是一个简单的 Promise 创建示例:

const myPromise = new Promise((resolve, reject) => {
    // 模拟一个异步操作,例如 setTimeout
    setTimeout(() => {
        const success = true;
        if (success) {
            resolve('操作成功');
        } else {
            reject('操作失败');
        }
    }, 1000);
});

在上述代码中,我们使用 setTimeout 模拟了一个异步操作,在 1 秒后判断 success 变量。如果 successtrue,则调用 resolve 函数,将 Promise 状态转变为 fulfilled,并传递成功信息 '操作成功'。如果 successfalse,则调用 reject 函数,将 Promise 状态转变为 rejected,并传递失败信息 '操作失败'。

处理 Promise 的结果

创建了 Promise 之后,我们需要处理它的结果,无论是成功还是失败。Promise 提供了 .then().catch() 方法来处理这些情况。

.then() 方法

.then() 方法用于处理 Promise 被 resolved 的情况。它接受两个可选参数,第一个参数是成功回调函数,当 Promise 被 resolved 时会调用这个函数,该函数接收 resolve 传递的值作为参数。第二个参数是失败回调函数,当 Promise 被 rejected 时会调用这个函数,该函数接收 reject 传递的原因作为参数。

以下是使用 .then() 方法处理上述 myPromise 的示例:

myPromise.then((value) => {
    console.log(value); // 输出:操作成功
}, (reason) => {
    console.error(reason); // 如果操作失败,输出:操作失败
});

在上述代码中,我们调用了 myPromise.then(),并传入了两个回调函数。当 myPromise 被 resolved 时,第一个回调函数会被调用,并将 resolve 传递的值打印到控制台。当 myPromise 被 rejected 时,第二个回调函数会被调用,并将 reject 传递的原因打印到控制台。

.catch() 方法

.catch() 方法是 .then() 方法第二个参数(失败回调函数)的一种简写形式,专门用于处理 Promise 被 rejected 的情况。使用 .catch() 方法可以使代码更加简洁,尤其是当我们只关心 Promise 失败的情况时。

以下是使用 .catch() 方法处理 myPromise 的示例:

myPromise.then((value) => {
    console.log(value); // 输出:操作成功
}).catch((reason) => {
    console.error(reason); // 如果操作失败,输出:操作失败
});

在上述代码中,我们使用 .catch() 方法捕获 myPromise 被 rejected 时的错误。这样做的好处是,当 myPromise 被 resolved 时,.catch() 方法不会被调用,代码结构更加清晰。

Promise 链

Promise 的一个强大功能是可以将多个 Promise 操作链接在一起,形成一个 Promise 链。这使得我们可以按照顺序执行多个异步操作,并且每个操作的结果可以作为下一个操作的输入。

要创建一个 Promise 链,我们只需要在 .then() 方法的成功回调函数中返回一个新的 Promise。例如,假设我们有两个异步操作,第一个操作获取用户信息,第二个操作根据用户信息获取用户的订单信息。

function getUserInfo() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({ name: '张三', age: 25 });
        }, 1000);
    });
}

function getOrderInfo(user) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const order = { user, orderId: '12345', amount: 100 };
            resolve(order);
        }, 1000);
    });
}

getUserInfo()
   .then((user) => {
        return getOrderInfo(user);
    })
   .then((order) => {
        console.log(order); // 输出:{ user: { name: '张三', age: 25 }, orderId: '12345', amount: 100 }
    })
   .catch((error) => {
        console.error(error);
    });

在上述代码中,我们首先定义了 getUserInfogetOrderInfo 两个函数,它们都返回一个 Promise。然后,我们调用 getUserInfo(),当它被 resolved 时,会将 resolve 传递的用户信息作为参数传递给第一个 .then() 方法的回调函数。在这个回调函数中,我们调用 getOrderInfo(user),并返回这个新的 Promise。当 getOrderInfo 被 resolved 时,会将 resolve 传递的订单信息作为参数传递给第二个 .then() 方法的回调函数,并打印到控制台。如果在这个过程中任何一个 Promise 被 rejected,.catch() 方法会捕获到错误并打印到控制台。

Promise 的静态方法

除了上述的实例方法外,Promise 还提供了一些静态方法,这些方法可以帮助我们更方便地处理多个 Promise。

Promise.all()

Promise.all() 方法用于将多个 Promise 包装成一个新的 Promise。这个新的 Promise 只有在所有传入的 Promise 都被 resolved 时才会被 resolved,并且它的 resolved 值是一个数组,包含了所有传入 Promise 的 resolved 值,顺序与传入的 Promise 顺序一致。如果其中任何一个 Promise 被 rejected,那么新的 Promise 会立即被 rejected,并且它的 rejection 原因就是第一个被 rejected 的 Promise 的 rejection 原因。

以下是一个使用 Promise.all() 的示例:

const promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('Promise 1 成功');
    }, 1000);
});

const promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('Promise 2 成功');
    }, 2000);
});

const promise3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('Promise 3 成功');
    }, 3000);
});

Promise.all([promise1, promise2, promise3])
   .then((values) => {
        console.log(values); // 输出:['Promise 1 成功', 'Promise 2 成功', 'Promise 3 成功']
    })
   .catch((error) => {
        console.error(error);
    });

在上述代码中,我们创建了三个 Promise,分别在 1 秒、2 秒和 3 秒后被 resolved。然后,我们使用 Promise.all() 将这三个 Promise 包装成一个新的 Promise。当所有三个 Promise 都被 resolved 时,.then() 方法的回调函数会被调用,并将包含三个 Promise resolved 值的数组打印到控制台。

Promise.race()

Promise.race() 方法同样用于将多个 Promise 包装成一个新的 Promise。与 Promise.all() 不同的是,Promise.race() 包装的新 Promise 会在第一个被 resolved 或者被 rejected 的 Promise 完成时就被解决。它的 resolved 值或者 rejection 原因就是第一个完成的 Promise 的 resolved 值或者 rejection 原因。

以下是一个使用 Promise.race() 的示例:

const promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('Promise 1 成功');
    }, 2000);
});

const promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject('Promise 2 失败');
    }, 1000);
});

const promise3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('Promise 3 成功');
    }, 3000);
});

Promise.race([promise1, promise2, promise3])
   .then((value) => {
        console.log(value);
    })
   .catch((error) => {
        console.error(error); // 输出:Promise 2 失败
    });

在上述代码中,promise2 在 1 秒后被 rejected,promise1 在 2 秒后被 resolved,promise3 在 3 秒后被 resolved。由于 promise2 是第一个完成的 Promise,并且它被 rejected,所以 Promise.race() 包装的新 Promise 也会被 rejected,.catch() 方法会捕获到 promise2 的 rejection 原因并打印到控制台。

Promise.resolve()

Promise.resolve() 方法用于创建一个已经被 resolved 的 Promise。它可以接受一个值或者另一个 Promise 作为参数。如果传入的是一个值,它会返回一个新的 Promise,这个 Promise 会立即被 resolved,并且 resolved 值就是传入的值。如果传入的是一个 Promise,它会直接返回这个 Promise。

以下是使用 Promise.resolve() 的示例:

const resolvedPromise = Promise.resolve('已解决的值');
resolvedPromise.then((value) => {
    console.log(value); // 输出:已解决的值
});

const anotherPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('延迟解决的值');
    }, 1000);
});

const wrappedPromise = Promise.resolve(anotherPromise);
wrappedPromise.then((value) => {
    console.log(value); // 输出:延迟解决的值
});

在上述代码中,我们首先使用 Promise.resolve('已解决的值') 创建了一个立即被 resolved 的 Promise,并在 .then() 方法中打印它的 resolved 值。然后,我们创建了一个延迟 1 秒后被 resolved 的 anotherPromise,并使用 Promise.resolve(anotherPromise) 将其包装。虽然 wrappedPromise 是通过 Promise.resolve() 创建的,但由于传入的是一个 Promise,所以 wrappedPromise 实际上就是 anotherPromise,1 秒后会打印出 '延迟解决的值'。

Promise.reject()

Promise.reject() 方法用于创建一个已经被 rejected 的 Promise。它接受一个参数作为 rejection 原因,返回的 Promise 会立即被 rejected,并且 rejection 原因就是传入的参数。

以下是使用 Promise.reject() 的示例:

const rejectedPromise = Promise.reject('已拒绝的原因');
rejectedPromise.catch((reason) => {
    console.error(reason); // 输出:已拒绝的原因
});

在上述代码中,我们使用 Promise.reject('已拒绝的原因') 创建了一个立即被 rejected 的 Promise,并在 .catch() 方法中捕获并打印它的 rejection 原因。

在 Node.js 中使用 Promise

在 Node.js 中,许多内置模块的异步操作都已经支持 Promise。例如,fs(文件系统)模块在 Node.js 10 版本之后就提供了基于 Promise 的 API。

使用 fs/promises 模块读取文件

假设我们有一个 example.txt 文件,内容为 'Hello, Node.js!'。我们可以使用 fs/promises 模块来读取这个文件,代码如下:

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

async function readFileContent() {
    try {
        const data = await fs.readFile('example.txt', 'utf8');
        console.log(data); // 输出:Hello, Node.js!
    } catch (error) {
        console.error(error);
    }
}

readFileContent();

在上述代码中,我们首先引入了 fs 模块的 promises 版本。然后,我们定义了一个异步函数 readFileContent,在这个函数中,我们使用 await fs.readFile('example.txt', 'utf8') 来读取文件内容。await 关键字只能在 async 函数内部使用,它会暂停当前 async 函数的执行,直到 Promise 被解决。如果 Promise 被 resolved,await 会返回 resolved 值,即文件内容。如果 Promise 被 rejected,await 会抛出错误,我们可以在 try...catch 块中捕获并处理这个错误。

使用 fs/promises 模块写入文件

同样,我们也可以使用 fs/promises 模块来写入文件。以下是一个示例,将字符串 'This is a new content' 写入到 newFile.txt 文件中:

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

async function writeFileContent() {
    try {
        await fs.writeFile('newFile.txt', 'This is a new content');
        console.log('文件写入成功');
    } catch (error) {
        console.error(error);
    }
}

writeFileContent();

在上述代码中,我们使用 await fs.writeFile('newFile.txt', 'This is a new content') 来写入文件。如果写入成功,await 会返回 undefined,我们会打印 '文件写入成功'。如果写入失败,await 会抛出错误,我们在 catch 块中捕获并处理这个错误。

在 Node.js 中处理多个异步操作

在实际应用中,我们经常需要处理多个异步操作。例如,我们可能需要先读取一个配置文件,然后根据配置文件中的信息读取另一个数据文件。这时,我们可以使用 Promise.all() 来处理多个基于 Promise 的异步操作。

假设我们有一个 config.json 文件,内容为 { "dataFile": "data.txt" },并且有一个 data.txt 文件,内容为 'Some data here'。以下是示例代码:

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

async function readConfigAndData() {
    try {
        const [config, data] = await Promise.all([
            fs.readFile('config.json', 'utf8'),
            fs.readFile((await JSON.parse(await fs.readFile('config.json', 'utf8'))).dataFile, 'utf8')
        ]);
        console.log('配置文件内容:', config);
        console.log('数据文件内容:', data);
    } catch (error) {
        console.error(error);
    }
}

readConfigAndData();

在上述代码中,我们使用 Promise.all() 同时执行两个异步操作:读取 config.json 文件和根据 config.json 文件中的信息读取 data.txt 文件。Promise.all() 会返回一个新的 Promise,当这两个异步操作都完成时,新的 Promise 会被 resolved,并且它的 resolved 值是一个数组,包含了两个异步操作的 resolved 值。我们使用数组解构来获取这两个值,并分别打印配置文件内容和数据文件内容。如果任何一个异步操作失败,Promise.all() 会立即被 rejected,我们在 catch 块中捕获并处理这个错误。

错误处理与最佳实践

在使用 Promise 进行异步编程时,正确的错误处理非常重要。以下是一些错误处理的最佳实践:

  1. 使用 .catch() 全局捕获错误:在 Promise 链的末尾添加 .catch() 方法,这样可以捕获到 Promise 链中任何一个环节抛出的错误。例如:
getUserInfo()
   .then((user) => {
        return getOrderInfo(user);
    })
   .then((order) => {
        console.log(order);
    })
   .catch((error) => {
        console.error('全局捕获到错误:', error);
    });
  1. 避免在 async 函数中遗漏 try...catch:当在 async 函数中使用 await 时,一定要使用 try...catch 块来捕获可能抛出的错误。例如:
async function readFileContent() {
    try {
        const data = await fs.readFile('nonexistentFile.txt', 'utf8');
        console.log(data);
    } catch (error) {
        console.error('读取文件时出错:', error);
    }
}
  1. 正确处理 Promise.all() 中的错误:在使用 Promise.all() 时,只要有一个 Promise 被 rejected,Promise.all() 就会被 rejected。因此,在 .catch() 方法中要处理可能的各种错误情况。例如:
Promise.all([promise1, promise2, promise3])
   .then((values) => {
        console.log(values);
    })
   .catch((error) => {
        console.error('Promise.all 出错:', error);
    });
  1. 避免未处理的 Promise 拒绝:在 Node.js 中,如果有未处理的 Promise 拒绝,Node.js 会抛出一个警告。为了避免这种情况,一定要确保所有的 Promise 都有相应的错误处理。可以通过监听 unhandledRejection 事件来捕获未处理的 Promise 拒绝,并进行适当的处理。例如:
process.on('unhandledRejection', (reason, promise) => {
    console.log('未处理的 Promise 拒绝:', reason, '来自 Promise:', promise);
});

总结

Promise 是 Node.js 中处理异步操作的重要工具,它通过更优雅的方式解决了传统回调函数带来的 “回调地狱” 问题。通过掌握 Promise 的基本概念、语法、静态方法以及在 Node.js 中的应用,我们可以编写出更易于维护和阅读的异步代码。同时,遵循错误处理的最佳实践,可以确保我们的应用程序更加健壮和稳定。在实际开发中,根据具体的需求选择合适的 Promise 操作方式,将有助于提高开发效率和代码质量。无论是简单的文件读取写入,还是复杂的多异步操作组合,Promise 都能提供强大而灵活的解决方案。