写在前面
深入JS腹地,体会Event Loop的精妙设计--JS基础篇(十一) - 掘金 (juejin.cn)
本期我们来看一个ES6中引入的重量级对象,Promise。在此之前,建议先理解事件循环机制(Event Loop)。⬆️⬆️⬆️
我们从以下三个方面来聊聊:
文末直达
正文
一、异步编程的发展
-
早期的同步执行: JS最初主要用于简单的页面交互和动态效果,执行靠单线程。这让代码只能顺序执行,页面有时会失去响应。
-
XMLHttpReequest的引入 XMLHttpReequest的引入,使得JS可以通过AJAX(异步的Javascript和XML)技术进行异步通信。JS进入了一个全新的时代。
-
回调函数: JS广泛采用回调函数来处理异步任务的结果,回调函数在异步任务完成后执行,返回数据或者报错等。
-
Promise的出现: 在ES6中,JS官方引入Promise对象作为处理异步任务的一种新方式。这极大的避免了回调函数带来的回调地狱等问题。
-
async/await的出现: 在ES8中,async/await语法诞生,进一步简化了异步任务的编写方式,使得异步代码更像同步代码,有更好的可读性和维护性。
以上异步编程机制和语法让JS在执行耗时操作时不会阻塞主线程,能够确保在处理网络请求时不影响用户与页面的交互,这对提高用户体验、程序性能具有重要意义。
二、传统异步编程带来的挑战
- 回调地狱(Callback Hell)
异步操作通常使用回调函数来处理,多个异步操作嵌套过深会导致代码结构复杂,难以维护。
// 回调地狱
function getUser(userId, callback) {
setTimeout(() => {
const user = { id: userId, name: 'John' };
callback(user);
}, 1000);
}
function getUserPosts(user, callback) {
setTimeout(() => {
const posts = [
{ userId: user.id, postId: 1, content: 'Post 1' },
{ userId: user.id, postId: 2, content: 'Post 2' }
];
callback(posts);
}, 1500);
}
function getComments(post, callback) {
setTimeout(() => {
const comments = [
{ postId: post.postId, commentId: 1, text: 'Comment 1' },
{ postId: post.postId, commentId: 2, text: 'Comment 2' }
];
callback(comments);
}, 2000);
}
在这个示例中,getUser,getUserposts,getComments都是模拟的一个异步任务,它们用于获取用户信息,用户发的帖子,用户帖子的评论。如果执行结果通过回调函数来传递,那么这种嵌套行为让代码结构变得复杂,不易扩展。就像这样:
// 使用回调函数处理多个异步操作
getUser(1, function(user) {
console.log('User:', user);
getUserPosts(user, function(posts) {
console.log('Posts:', posts);
const firstPost = posts[0];
getComments(firstPost, function(comments) {
console.log('Comments for post 1:', comments);
});
});
});
当多个异步操作依次执行,并且需要依赖前一次异步操作的结果时,若使用传统的回调函数处理,会导致异步代码呈现深层次的调用,进而形成回调地狱。那么有解决办法吗?当然有,继续看看传统异步编程的局限性。
- 错误处理的复杂化
在异步编程中,异步任务的回调函数的执行时机会受到多种因素的影响(网络延迟、资源可用性等)。这种不确定性可能导致错误发生时,当前代码的执行上下文已经无法约束错误所在区域,导致错误的处理变得更加困难。
- 并发(竞态)
多个异步操作同时进行(在浏览器或Node.js环境),也就是多线程并发的状态下,多个异步任务中的共享数据可能在不同的时间点被读取或者修改,会导致各异步任务的数据不同步,导致结果的正确性受到影响。
🌰:假设有两个异步操作 A 和 B 都要读取和修改同一个变量。如果操作 A 读取变量后,操作 B 立即也读取并修改了该变量,那么操作 A 的修改可能会被操作 B 覆盖,导致最终的数据状态与预期不符。
三、Promise
Promise是ES6中引入的一种处理异步操作的对象,他实际上包含了异步操作的结果以及状态。当我们使用Promise处理异步任务时,只需要向构造函数Promise()中传入一个执行器,这个执行器函数里面应该包含了整个异步任务的逻辑。
1️⃣Promise的状态
Promise是一尚未完成但是最终会完成的内置对象,它可以处于以下三个状态:
- pending(待定):初始状态,既不是成功也不是失败。
- fulfilled(已成功):表示异步操作完成且成功。
- rejected(已失败):表示异步操作完成但失败了,通常是因为出现错误。
以上三个状态构成了Promise这个对象的完整生命周期。
2️⃣Promise构造函数的方法
new Promise(executor);
如上,Promise的构造函数接收一个执行器(executor function),创建对象时,该函数在其内部立即执行,即使Promise对象还未创建完成。执行器函数里包含2个参数。resolve和reject,这两个参数是构造函数原型里的方法,用于改变Promise的状态:
示例:
//箭头函数为执行器
const promise = new Promise((resolve, reject) => {
//模拟异步操作逻辑
setTimeout(() => {
const success = true;
if (success) {
resolve('Success!');
} else {
reject('Failed!');
}
}, 1000);
});
-
状态1 resolve(value):当异步操作成功时调用该函数,将Promise的状态由
pending改为fulfilled,并且将value作为参数传递给.then()方法的回调函数。 -
状态2 reject(reason):当异步操作成功时调用此函数,将Promise的状态由
pending改为rejected,并将reason作为参数传递给.catch()或者.then()的回调函数。
3️⃣Promise的实例方法
Promise对象中有几个实例方法用于处理异步操作成功或失败后的工作。
1.then(onFulfilled,onRejected)
Promise.prototype.then()方法允许我们在异步操作结束后分别执行不同的函数。他接受两个参数:
-
onFulfilled:当Promise状态变为fulfilled(已成功)时调用的函数。这个函数接收resolve函数传递的值作为参数。
-
onRejected:当Promise状态变为rejected(已失败)时调用的函数。这个函数接收reject函数传递的错误作为参数。
这两个参数都是可选参数,省略的那个在.then()方法中将不被调用。
const promise = new Promise((resolve, reject) => { resolve('Success');
});
promise.then(
result => console.log(result), // 输出 "Success"
error => console.error(error)
);
2.catch(onRejected)
Promise.prototype.catch()是.then()方法的一个简写形式,只用于Promise对象中异步任务失败的情况。它等价于调用.then(null,onRejected)
const promise = new Promise((resolve, reject) => {
reject(new Error('Failure'));
});
promise.catch(error => console.error(error)); // 输出 "Error:Failure"
3..finally(onFinally)
Promise.prototype.finally() 方法允许你指定一个回调函数,无论 Promise 的最终状态是成功还是失败,这个函数都会被执行。这在需要进行一些清理工作,如关闭文件、释放资源等场景下非常有用。
这里的onFulfilled、onRejected、onFinally都是函数,同时也作为Promise在异步操作结束后的行为代表。
来看一下流程图:
流程:
- pending:promise开始处于
pending状态。 - Operation(async):执行异步操作,如网络请求、定时器等。
- Success:如果操作成功,则Promise状态变为
resolved。 - Failure:如果操作失败,则Promise状态变为
rejected。 - Promise(resolved):如果Promise状态为resolved,调用
then方法。 - Promise(rejected):如果Promise状态为rejected,调用
catch方法。
4️⃣Promise的链式调用
我们着重看一下.then()这个方法,这个方法主要用于链接异步操作,由Promise对象调用。来看一个例子:
function first() {
//三种状态 pending准备 resolved触发 rejected
return new Promise((resolve,reject) => {//status:pending
setTimeout(() => {
console.log("我的掘金1级了");
resolve();//改变状态
}, 2000);
})
}
function third(){
console.log("我的掘金3级了");
}
function second() {
return new Promise((resolve,reject) => {
setTimeout(() => {
console.log("我的掘金2级了");
resolve();
}, 1000);
})
}
要如何调用才能正确打印结果呢。我们来试一试:
first().then(() => {
second();
}).then(()=>{
third();
});
//掘金1级
//掘金3级
//掘金2级
这样调用正确吗?
有点小错误。我们联系到Promise的执行和返回机制想想。
在Pomise链中,每个.then()方法都会返回一个新的Promise对象,新的Promise对象的状态由前一个Promise对象的执行结果决定。当我们连续调用.then()方法,将一系列异步操作串联起来形成了一个链式结构。
上面的错误关键在于:
second()函数没有正确返回,导致第二个.then没有找到second()的Promise,反而顺着Promsie链找到了前面的Promise,且状态为resolved,所以第二个.then()里的third()在打印完第一句后直接执行了。
这样修改后就好了:
first().then(() => {
return second();
}).then(()=>{
third();
});
我们再详细梳理一遍:
- first()
- 返回第一个Promise,在2秒后resolved
- 第一个.then()
- 当first()的Promise状态为resolved时,回调函数被执行。
- 在这个回调函数中,继续调用
second(),并且返回它的Promise。 - 由于
second()的Promise,Promise链会等待second()的Promise状态改变。
- 第二个.then()
second()的状态改变为resolved后,回调函数被执行。- 回调函数在second()执行完后调用
third()
.then()达到了将异步代码捋成同步代码的效果。*
4️⃣async/await语法糖
async/await语法糖是ES8引入的,是一种建立在Promise之上的更为直观的异步编程方式。
之前的写法可以改为这样:
//部分
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function first() {
await delay(2000);
console.log("我的掘金1级了");
}
调用时只需要await一下,执行顺序就可以严格规定好。
sync function execute() {
await first();
await second();
third();
}
这样,有着同步代码的直观,这样就不需要嵌套或者链式调用.then和.catch了。
另外,对于async/await语法糖,可以使用try/catch来捕获错误:
async function execute() {
try {
await first();
await second();
await third();
} catch (error) {
console.error("出错了:", error);
throw(error);
}
}
这样,所有异步任务的错误都能被捕获到。
总结
以上就是所有内容。不过,关于Promise的用法还有很多很多,大家可以去Promise - JavaScript | MDN (mozilla.org)看看👀。如果你觉得本文对你有帮助,还请点个赞,这将是我持续创作的动力。