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

JavaScript期约(Promise)的基本概念

2023-06-047.3k 阅读

JavaScript 期约(Promise)的基本概念

什么是 Promise

在 JavaScript 中,Promise 是一种用于处理异步操作的对象。异步操作是指那些不会立即完成的操作,例如网络请求、读取文件等。在 Promise 出现之前,JavaScript 处理异步操作主要依赖回调函数。然而,回调函数在处理多个异步操作时,很容易出现回调地狱(Callback Hell)的问题,即多层嵌套的回调函数使得代码难以阅读和维护。

Promise 的出现为解决这个问题提供了一种更优雅的方式。Promise 代表一个异步操作的最终完成(或失败)及其结果值。它有三种状态:

  1. Pending(进行中):初始状态,既不是已成功,也不是已失败。
  2. Fulfilled(已成功):意味着操作成功完成,Promise 会有一个 resolved value(解析值)。
  3. Rejected(已失败):意味着操作失败,Promise 会有一个 rejection reason(拒因)。

一旦 Promise 进入 FulfilledRejected 状态,就永远不会再改变,这一特性保证了异步操作结果的确定性。

创建 Promise

可以通过 new Promise() 构造函数来创建一个 Promise 对象。Promise 构造函数接受一个执行器函数作为参数,该执行器函数会立即执行。执行器函数接受两个参数:resolvereject

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

在上述代码中,使用 setTimeout 模拟了一个异步操作,1 秒后根据 success 的值决定是调用 resolve 还是 reject。如果 successtrue,则调用 resolve 并传递成功消息;如果为 false,则调用 reject 并传递失败消息。

处理 Promise 的结果

then 方法

Promise 对象提供了 then 方法来处理异步操作的结果。then 方法接受两个回调函数作为参数,第一个回调函数用于处理 Fulfilled 状态(成功),第二个回调函数用于处理 Rejected 状态(失败)。

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

在上面的代码中,myPromise 成功时,then 方法的第一个回调函数会被调用,并传入 resolve 传递的值,这里会在控制台输出 “操作成功”。如果 myPromise 失败,then 方法的第二个回调函数会被调用,并传入 reject 传递的拒因。

catch 方法

catch 方法是一种更简洁的处理 Rejected 状态的方式。它实际上是 then(null, rejectionCallback) 的语法糖。

myPromise.then((value) => {
    console.log(value);
}).catch((reason) => {
    console.error(reason);
});

上述代码和前面使用 then 方法完整传递两个回调函数的效果是一样的,catch 方法使得代码更加简洁,专注于错误处理。

Promise 的链式调用

Promise 的一个强大特性是可以进行链式调用。每个 then 方法返回一个新的 Promise 对象,这使得我们可以将多个异步操作串联起来,每个操作依赖前一个操作的结果。

function asyncOperation1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('操作 1 完成');
        }, 1000);
    });
}

function asyncOperation2(result1) {
    return new Promise((resolve) => {
        setTimeout(() => {
            const combinedResult = result1 + ',操作 2 完成';
            resolve(combinedResult);
        }, 1000);
    });
}

function asyncOperation3(result2) {
    return new Promise((resolve) => {
        setTimeout(() => {
            const combinedResult = result2 + ',操作 3 完成';
            resolve(combinedResult);
        }, 1000);
    });
}

asyncOperation1()
   .then(asyncOperation2)
   .then(asyncOperation3)
   .then((finalResult) => {
        console.log(finalResult);
    })
   .catch((error) => {
        console.error(error);
    });

在这个例子中,asyncOperation1 先执行,1 秒后完成并返回结果。then 方法将这个结果传递给 asyncOperation2asyncOperation2 基于前一个结果进行操作,1 秒后完成并返回新的结果。这个过程继续传递给 asyncOperation3。如果任何一个 Promise 被拒绝,catch 方法会捕获并处理错误。

Promise.all

Promise.all 方法用于将多个 Promise 实例包装成一个新的 Promise 实例。新的 Promise 实例在所有传入的 Promise 实例都变为 Fulfilled 状态时才会变为 Fulfilled,此时新 Promise 的 resolved value 是一个包含所有传入 Promise resolved value 的数组。如果其中任何一个 Promise 变为 Rejected 状态,新 Promise 就会立即变为 Rejected 状态,其拒因就是第一个被拒绝的 Promise 的拒因。

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

const promise2 = new Promise((resolve) => {
    setTimeout(() => {
        resolve('Promise 2 完成');
    }, 1500);
});

const promise3 = new Promise((resolve) => {
    setTimeout(() => {
        resolve('Promise 3 完成');
    }, 2000);
});

Promise.all([promise1, promise2, promise3])
   .then((results) => {
        console.log(results);
    })
   .catch((error) => {
        console.error(error);
    });

在上述代码中,Promise.all 接受一个包含三个 Promise 的数组。由于所有 Promise 最终都会成功,then 方法的回调函数会在所有 Promise 都完成后被调用,results 数组会包含三个 Promise 的 resolved value。如果其中有一个 Promise 失败,catch 方法会捕获错误。

Promise.race

Promise.race 方法同样用于将多个 Promise 实例包装成一个新的 Promise 实例。但与 Promise.all 不同的是,Promise.race 只要传入的 Promise 实例中有一个率先改变状态(无论是 Fulfilled 还是 Rejected),新的 Promise 就会跟着改变状态,其 resolved value 或 rejection reason 就是第一个改变状态的 Promise 的 resolved value 或 rejection reason。

const promiseA = new Promise((resolve) => {
    setTimeout(() => {
        resolve('Promise A 完成');
    }, 2000);
});

const promiseB = new Promise((resolve) => {
    setTimeout(() => {
        resolve('Promise B 完成');
    }, 1000);
});

const promiseC = new Promise((resolve) => {
    setTimeout(() => {
        resolve('Promise C 完成');
    }, 1500);
});

Promise.race([promiseA, promiseB, promiseC])
   .then((result) => {
        console.log(result);
    })
   .catch((error) => {
        console.error(error);
    });

在这个例子中,promiseB 会率先完成,所以 Promise.race 返回的新 Promise 会以 promiseB 的 resolved value 作为自己的 resolved value,then 方法的回调函数会输出 “Promise B 完成”。

Promise.resolve 和 Promise.reject

Promise.resolve

Promise.resolve 方法用于创建一个已成功的 Promise 对象。它可以接受一个值作为参数,这个值会成为新 Promise 的 resolved value。如果传入的参数本身就是一个 Promise 对象,则直接返回该 Promise 对象。

const resolvedValue = '直接成功的值';
const resolvedPromise = Promise.resolve(resolvedValue);

resolvedPromise.then((value) => {
    console.log(value);
});

在上述代码中,Promise.resolve 创建了一个已成功的 Promise,其 resolved value 为 resolvedValuethen 方法的回调函数会输出 “直接成功的值”。

Promise.reject

Promise.reject 方法用于创建一个已失败的 Promise 对象。它接受一个参数作为拒因,这个拒因会在 Promise 被拒绝时传递给 catch 方法或 then 方法的第二个回调函数。

const rejectionReason = '直接失败的原因';
const rejectedPromise = Promise.reject(rejectionReason);

rejectedPromise.catch((reason) => {
    console.error(reason);
});

在上述代码中,Promise.reject 创建了一个已失败的 Promise,其拒因为 rejectionReasoncatch 方法的回调函数会输出 “直接失败的原因”。

深入理解 Promise 的执行机制

JavaScript 是单线程语言,但它通过事件循环(Event Loop)机制来处理异步操作。Promise 的执行过程与事件循环密切相关。

当执行到一个 Promise 时,它的执行器函数会立即同步执行。如果在执行器函数中调用了 resolvereject,并不会立即触发 thencatch 方法中的回调函数。相反,这些回调函数会被放入微任务队列(Microtask Queue)中。

事件循环会不断检查调用栈(Call Stack)是否为空。当调用栈为空时,事件循环会优先处理微任务队列中的任务,将微任务队列中的回调函数依次压入调用栈执行。只有当微任务队列也为空时,事件循环才会处理宏任务队列(Macrotask Queue)中的任务。

例如:

console.log('开始');

const myPromise = new Promise((resolve) => {
    console.log('Promise 执行器');
    resolve();
});

myPromise.then(() => {
    console.log('Promise then 回调');
});

console.log('结束');

在这段代码中,首先输出 “开始”,然后进入 Promise 的执行器函数,输出 “Promise 执行器” 并调用 resolve。此时,then 方法的回调函数被放入微任务队列。接着输出 “结束”,当调用栈为空时,事件循环处理微任务队列,执行 then 方法的回调函数,输出 “Promise then 回调”。所以最终的输出顺序是:“开始”,“Promise 执行器”,“结束”,“Promise then 回调”。

Promise 在实际项目中的应用场景

网络请求

在前端开发中,经常需要通过 AJAX 或 Fetch 进行网络请求,这是 Promise 非常常见的应用场景。例如,使用 Fetch API 进行网络请求:

fetch('https://example.com/api/data')
   .then((response) => {
        if (!response.ok) {
            throw new Error('网络请求失败');
        }
        return response.json();
    })
   .then((data) => {
        console.log(data);
    })
   .catch((error) => {
        console.error(error);
    });

在这个例子中,fetch 返回一个 Promise。第一个 then 方法检查响应状态,如果状态不是 2xx,则抛出错误,否则将响应解析为 JSON 数据。第二个 then 方法处理解析后的数据,catch 方法捕获任何请求过程中的错误。

文件读取

在 Node.js 中,读取文件也是一个异步操作,可以使用 Promise 来处理。例如:

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

const readFileAsync = promisify(fs.readFile);

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

这里使用 util.promisify 将 Node.js 的 fs.readFile 这个基于回调的异步函数转换为返回 Promise 的函数。然后通过 thencatch 方法处理文件读取的结果和错误。

总结 Promise 的优势

  1. 解决回调地狱:通过链式调用和清晰的错误处理机制,使得异步代码更易于阅读和维护,避免了多层回调嵌套带来的混乱。
  2. 更好的错误处理catch 方法提供了统一的错误处理入口,能够捕获链式调用中任何一个 Promise 抛出的错误,使得错误处理更加集中和便捷。
  3. 异步操作的组合Promise.allPromise.race 等方法提供了强大的异步操作组合能力,方便处理多个异步操作的并发或竞争关系。
  4. 与事件循环的协同:Promise 的执行机制与 JavaScript 的事件循环紧密结合,能够在保证单线程运行的前提下,高效地处理异步任务。

综上所述,Promise 为 JavaScript 处理异步操作提供了一种高效、优雅且强大的方式,在现代前端和后端开发中都有着广泛的应用。无论是简单的网络请求,还是复杂的异步任务编排,Promise 都能发挥重要作用,帮助开发者编写更健壮、更易维护的代码。掌握 Promise 的基本概念和使用方法,是成为一名优秀 JavaScript 开发者的重要一步。在实际开发中,还需要结合具体的业务场景,灵活运用 Promise 的各种特性,以实现高效、可靠的异步编程。同时,随着 JavaScript 语言的发展,async/await 语法糖的出现进一步简化了 Promise 的使用,它基于 Promise 构建,使得异步代码看起来更像同步代码,这也是开发者需要深入学习和掌握的内容,以便在不同的场景下选择最合适的异步编程方式。

在实际项目中,还可能会遇到一些复杂的情况,比如 Promise 嵌套、处理大量并发请求等。对于 Promise 嵌套,要尽量避免,通过合理的链式调用和函数封装来优化代码结构。当处理大量并发请求时,可以结合 Promise.allPromise.race 的特性,并根据实际需求进行调整。例如,如果需要控制并发请求的数量,可以使用队列的方式,分批处理请求,保证系统资源的合理利用。另外,在错误处理方面,除了使用 catch 方法进行全局捕获外,还可以在具体的 Promise 操作中进行更细粒度的错误处理,以确保错误信息能够准确反馈给开发者,便于调试和修复问题。总之,深入理解和熟练运用 Promise 的各种概念和技巧,对于提升 JavaScript 开发能力和项目质量至关重要。