面试必考题之Promise——解决回调地狱
前言
在当今高度依赖于交互性和实时数据处理的互联网应用开发中,异步编程已成为不可或缺的一部分。它允许程序在等待某个耗时操作(例如网络请求、数据库查询)完成的同时,继续执行其他任务,从而显著提升了应用程序的响应速度与用户体验。然而,随着异步操作的增多,传统基于回调函数的处理方式逐渐暴露出了其局限性,尤其是当多个异步操作需要顺序执行或相互依赖时,"回调地狱"——即层层嵌套的回调函数,使得代码变得难以阅读、理解和维护。
而正是因为许多程序员受到回调地狱的侵害
ES6中的原生对象Promise应运而生
正文
要讲Promise,我们先搞清楚几个基本概念
什么是异步
异步(Asynchronous)编程是一种编程模式,它允许程序在等待某个操作(比如I/O操作、网络请求、数据库查询等)完成的同时,继续执行其他任务,而不需要阻塞或暂停整个程序的执行。在现代软件开发,尤其是在Web开发中,异步编程尤为重要,因为它有助于提升应用的响应性和用户体验。
为什么需要异步?
计算机执行任务时,资源(如CPU、内存、网络)的使用是有限的。在执行一些耗时操作(如读取硬盘上的大文件、跨网络请求数据)时,如果采用同步方式,程序会等待这些操作完成才继续执行下一步,这期间CPU等资源可能处于闲置状态,造成效率低下。特别是在单线程环境中(如JavaScript在浏览器中的执行环境),同步操作会导致整个页面或应用暂时无响应,用户体验不佳。
现在我们来举个例子
var a = 1
setTimeout(()=>{
console.log(a);
},1000)
console.log(a);
这就是一个最为简单的异步任务
我们用setTimeout()
将指定程序设置为1秒后执行,这样允许程序在等待某些操作(如I/O操作、长时间计算或定时任务)完成的同时,继续执行其他任务,而不是阻塞主线程等待该操作结束。这对于保持用户界面的响应性和提高程序整体效率至关重要,*尤其是在单线程环境下,而JavaScript在浏览器环境下就是一个单线程环境
在这一段代码下:
var data = null
function a() {
setTimeout(function() {
data = 'hello'
b()
},1000)
}
function b() {
console.log(data);
}
a()
b函数的实现需要通过a函数的异步任务执行时才能够执行,
否则单独调用b函数则会输出null
这样一个小型的回调程序还算简洁
但若是像这样呢?
function a(cbB,cbC,cbD) {
cbB(cbC,cbD)
}
function b(cb,cbD) {
cb(cbD)
}
function c(cb) {
cb()
}
function d() {
}
a(b,c,d)
是不是就开始发晕?
而在大型企业中,比这样复杂的需求多得多。
程序员们经常需要在一个大型的封装函数里使用几十上百个参数
而当上一任程序员把程序交给你后,你感动吗?
那肯定是不敢动啊!这就是回调地狱
由于过度使用回调函数来处理一系列的异步操作,导致代码结构变得极其复杂、难以理解和维护。
所以ES6中处理异步任务的对象应运而生——Promise
Promise是JavaScript中用于处理异步操作的一个对象,它代表了一个最终可能会完成(或失败)的异步操作及其结果。Promise的设计旨在让异步代码的编写更加有序和易于理解,相较于传统的回调函数,它能有效地解决“回调地狱”问题,使异步逻辑看起来更像是同步代码。
Promise的主要特点包括:
- 状态不可变性:Promise有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。一旦Promise从pending变为fulfilled或rejected,这个状态就固定不变了,任何操作都无法再改变这个状态,这确保了异步操作结果的确定性。
- 链式调用:Promise通过
.then
和.catch
方法支持链式操作,使得多个异步操作可以按照一定的顺序依次执行,每个步骤都可以根据前一个步骤的成功或失败来决定自己的行为,大大增强了代码的可读性和灵活性。 - 错误处理集中:使用
.catch
方法可以统一处理Promise链中的错误,避免了在每个回调函数中都需要单独处理错误的情况,使得错误处理更加集中和统一。 - 避免回调地狱:通过Promise的链式调用和错误处理机制,可以有效减少因多层嵌套回调函数而形成的“回调地狱”,使得代码结构更加扁平和清晰。
- 静态方法:Promise还提供了静态方法如
Promise.all()
、Promise.race()
、Promise.resolve()
和Promise.reject()
等,这些方法进一步丰富了异步编程的工具集,使得处理并发异步操作或快速创建已完成/已拒绝的Promise变得简单。
用文字来描述有些苍白,我们现在来用代码说话
function xq() {
setTimeout(() => {
console.log('相亲');
},2000)
}
function marry() {
setTimeout(() => {
console.log('结婚');
},1000)
}
xq()
marry()
我们定义两个函数,一个叫相亲xq()
,一个叫结婚marry()
现在我们运行这两个函数,由于setTimeout()
处理异步任务,
就会先结婚后相亲,这怎么合理呢?
于是,为了更好的处理异步任务,我们使用Promise对象来调用这两个函数
function xq() {
return new Promise((resolve,reject)=>{
setTimeout(() => {
console.log('相亲');
resolve();
},2000)
})
}
function marry() {
return new Promise((resolve,reject)=>{
setTimeout(() => {
console.log('结婚');
resolve();
},1000)
})
}
xq().then(() => {
marry()
})
我们使用Promise()
构造函数生成一个promise实例对象,并返回这样一个实例对象
而在setTimeout
中的resolve()
执行前,该函数内的promise
对象一直是处于pending状态
在函数中的resolve()
执行后,函数内的promise
对象就变成了resolve状态
接着.then()
开始链式调用,访问到xq()
传回来了resolve状态,便开始执行内部函数marry()
最后就先相亲后结婚了
我们再添加一个函数baby()
体验一下更加高级的链式调用
function baby() {
console.log("宝宝");
}
这个函数并不是异步的,但是我们想让他异步执行,所以我们继续使用promise
实例对象让它运行
xq().then(() => {
marry().then(() => {
baby();
})
})
这样写是完全没有毛病的,但是一层包着一层的写法仍然看着有些繁琐
所以我们还可以写成这样
xq()
.then(() => {
marry()
})
.then(()=> {
baby()
})
promise
里的.then()
是可以这样调用的,但是有一个问题,你会发现在这个程序中输出的是
为什么呢?
是因为当.then()
是有自己的默认返回值的
第二个.then()
访问到then返回的是pending会进行链式访问,访问到最上面xq()
的resolve状态,所以宝宝会比结婚先输出
而当我们把代码改成
xq()
.then(() => {
return marry()
})
.then(()=> {
baby()
})
.then()
检测到内部函数有返回值,便不使用自身的返回值,而使用内部函数的返回值,让下一个.then()
访问到它后会等待它的resolve状态再运行。
promise对象中还有一个catch
异常处理函数,它的使用一样是异步的
它是为了捕获promise
中的另一个状态reject
而生的
它的使用可以看一下这样一串代码
var data = null
function a() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('a complete');
data = 'hhhh'
reject()
}, 1000)
})
}
function b() {
console.log(data);
}
a()
.then((res) => {
console.log(res);
b()
})
.catch((err) => {
console.log(err,'xxxx');
})
本身是会报错的,但是由于有了catch()
进行了异常处理,使得不会输出报错,而是输出程序中设定的内容。
总结
Promise通过提供一种标准化的方式来组织异步操作,不仅提高了代码的可维护性和可读性,也使得开发者能够以更加符合直觉的方式处理复杂的异步逻辑。它是现代JavaScript异步编程的核心之一,广泛应用于网络请求、文件操作、定时任务等各种需要异步处理的场景。
求点赞评论收藏,有问题随时私信博主!
转载自:https://juejin.cn/post/7385776238491877413