前言
在JavaScript
的世界里,异步编程一直是一个核心而复杂的议题。随着Web应用的日益复杂化,处理异步操作的需求也愈发迫切。在早期,我们依赖于回调函数(callbacks
)来处理异步任务,但随着代码逻辑的嵌套层级不断增加,所谓的“回调地狱”(callback hell
)问题逐渐凸显,使得代码难以维护和理解。
为了解决这个问题,JavaScript
引入了Promise
,一个代表异步操作最终完成或失败的对象。Promise的出现,不仅极大地简化了异步编程的复杂性,还提高了代码的可读性和可维护性。它允许我们将异步操作的结果封装在一个对象中,并可以在未来的某个时间点通过.then()
和.catch()
方法来处理成功或失败的结果,本章将带你深入探索JavaScript
中的Promise
机制。从Promise
的基本概念、构造方法,到链式调用、错误处理
js是单线程语言
那为什么js
要设计为单线程语言呢,因为最初设计的时候是想让这么语言成为浏览器的脚本语言,因此要将js
设置的便捷,性能高,对用户运行内存占用要低,于是js
就被打造成单线程语言,而单线程语言,顾名思义就是一次只能干一件事情,相反还有多线程语言,比如java
,一次可以干多件事,执行效率快但是开销的性能也多
异步
let a = 1
console.log(a,2);
setTimeout(() => {
a = 2
console.log(a,5);
}, 1000)
console.log(a,7);
我们看这段代码,我们先创建了一个变量a
,然后输出,接着我们放一个定时器,后面的1000
代表1000毫秒
,定时器内将变量a
的值改为了2
,最后我们在定时器后面输出a
,为了更清楚代码的执行顺序,我在每行变量a
的执行后面都标记了代码的a
代码所在位置
那我们来想想第七行的a
会输出什么呢,有两种可能,一是按正常从上往下的顺序,第七行代码等待定时器执行完毕,还有一种可能是代码执行时发现这个定时器是耗时的,需要等待一秒,那就先将它放置一边,先执行后面的,最后再来执行这段代码,让我们看看结果,验证一下是那种执行方式
可以看到先执行了第七行代码,再执行了定时器里面的第五行代码,就是我们猜想的第二种方法,那为什么不用第一种方法呢,你想想看,如果后面要执行的代码和前面这个要耗时的代码没有半毛钱关系,但是按顺序来必须等这个耗时的代码先执行完毕,那其它不耗时的代码就一直被卡着,相当于一粒老鼠屎坏了一锅粥,那么这门编程语言的执行效率就太慢了,肯定就被淘汰掉了,
而第二种方法就是js遇到需要耗时执行的代码就将其先挂起,等到后续不耗时的代码执行完毕后再执行挂起的耗时代码,这种方式就叫异步编程
解决异步
let a = 1
console.log(a,2);
setTimeout(() => {
a = 2
console.log(a,5);
}, 1000)
console.log(a,7);
还是这段代码,如果我们想要第七行的变量a
输出一个2
,那我们怎么解决v8
中的异步编程呢?有人会觉得你要执行a
输出为2
,直接把定时器给除掉不就好了,但其实在真正开发的时候,我们也会遇到耗时的代码的,这里我们只是用定时器进行模拟。
在Promise
方法打造之前,我们最早就是用回调来解决异步这个问题的
回调
function a() {
SetTimeout(() => {
console.log('a 执行完毕');
}, 1000)
}
function b() {
console.log('b 执行完毕');
}
a()
b()
我们打造两个函数,a
里面是一个耗时的定时器,然后我们先调用a
再调用b
,那会先执行a
还是b
呢,我们来看看输出结果
可以看到这里还是进行了异步操作,先输出了后调用的b,那如果我们想先执行a再执行b呢,那我们就可以将b的调用放入a当中
function a() {
setTimeout(() => {
console.log('a 执行完毕');
b()
}, 1000)
}
function b() {
console.log('b 执行完毕');
}
a()
这样子的话,a
不执行,b
就无法执行,只有等a
开始执行执行到b
的调用才会开始执行b
,让我们看看代码输出结果
可以看到先执行了
a
再执行的b
,这种将函数调用放入耗时函数里面的手段就叫作回调
稍微更优雅的一种写法是这样的
function a(cb) {
setTimeout(() => {
console.log('a 执行完毕');
cb()
}, 1000)
}
function b() {
console.log('b 执行完毕');
}
a(b)
这种方法很好理解,但是有弊端,我们看看下面例子
function a(cb, cb2, cb3) {
setTimeout(() => {
console.log('a 执行完毕');
cb(cb2, cb3)
}, 1000)
}
function b(cb2, cb3) {
setTimeout(() => {
console.log('b 执行完毕');
cb2(cb3)
}, 500)
}
function c(cb3) {
setTimeout(() => {
console.log('c 执行完毕');
cb3()
}, 1500)
}
function d() {
console.log('d 执行完毕');
}
a(b, c, d)
上面的代码我们创建了四个函数,我们想按a,b,c,d
的顺序执行那我们需要在调用函数a
的时候,将函数b,c,d
放入a
当中去,然后在a
中调用b
,在调用b的时候将c,d
放入b的调用中,在调用函数c
的时候再将d
放入函数c
中去,这么传下去,那是不是一下子就觉得很麻烦,感觉很乱,那如果一个项目如果有一百个函数,第一百个函数非要等第九十九个函数执行完,九十九个函数非要等九十八个函数执行完,那我们是不是就直接疯了。而用这种回调方法解决异步形成很复杂的函数关系,嵌套过深,在业内有一个名称形容它回调地狱"(Callback Hell)
最后打印的结果
那如果最后
d
没有打印出来,那你知道哪里出了问题吗,你肯定不知道,那你就必须一个一个的查,这样就特别麻烦,于是官方被程序员戳脊梁骨,催促打造一个好用点的方法出来,于是官方顶不住了,在es6
中更新了很多新的东西,也就是我们今天的主角Promise
Promise
先看下面一段异步代码
function xq() {
setTimeout(() => {
console.log('彭于晏相亲了');
}, 2000)
}
function marry() {
console.log('彭于晏结婚了');
}
xq()
marry()
这段代码会先执行marry
再执行xq
,下面是执行结果
那不行啊,就算是彭于晏也不能结完婚再去相亲,这不得被打断腿,为了拯救彭于晏,于是我们要将流程改一下,先相亲再结婚,除了我们刚刚讲到的回调,还可以用
es6
新打造的方法Promise
,让我们看看怎么用的
function xq() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('彭于晏相亲了');
resolve()
}, 2000)
})
}
function marry() {
console.log('彭于晏结婚了');
}
xq().then(() =>{
marry()
})
在彭于晏相亲的时候我们要返回es
这个打造出来的新方法(也就是构造函数)创建出来的对象,构造函数中接收一个函数,我们这里写了一个箭头函数,里面的函数需要接收两个形参,这两个形参可以随便写,但是官方是resolve
和reject
,一个是解决,一个是拒接,也比较语义化。所有我们最好按官方来。
然后我们调用xq
函数返回的就是new Promise
创建出来的实例对象,而这个实例对象中有then
这个方法,这个方法里面也接收一个回调函数,这时候还不行,现在就要讲到那两个形参的作用,调用resolve
代表函数顺利执行,reject
代表执行失败,我们加上resolve()
,表示成功,然后我们就可以正常使用实例对象调用then
方法。我们来看看成功拯救了彭于晏吗
成功了,我们将顺序调回来了,那有人会觉得代码变复杂了,但其实复杂这一点对我们熟悉后基本没什么影响,比将一推函数作为形参传来传去好多了。
那结完婚了,小彭就该出生了,我们这样写行吗
function xq() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('彭于晏相亲了');
resolve()
}, 2000)
})
}
function marry() {
setTimeout(() => {
console.log('彭于晏结婚了');
resolve()
}, 1000)
}
function bady() {
console.log('小彭出生了');
}
xq().then(() => {
marry()
bady()
})
看看执行结果
有点太刑了,怎么小彭先出来了
那我们再来拯救一下彭于晏,在marry
函数上,我们也要给它一个Promise
,让它具有调用then
的能力,然后将bady
放入marry
调用的then
方法的函数体里面,就变成下面这样了
function xq() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('彭于晏相亲了');
resolve()
}, 2000)
})
}
function marry() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('彭于晏结婚了');
resolve()
}, 1000)
})
}
function bady() {
console.log('小彭出生了');
}
xq().then(() => {
marry().then(() => {
bady()
})
})
这时候流程就对了,我们再一次拯救了彭于晏。下面函数调用更优雅一点的写法是这样的
xq()
.then(() => {
return marry()
})
.then(() => {
bady()
})
将函数返回出来再调用.then
,看着是不是就清晰明朗多了,这就是Promise
的用法,清晰的解决了js
中异步编程的问题
结语
在JavaScript
中,异步编程是开发过程中不可或缺的一部分。从最初的回调函数到Promise
的引入,我们见证了异步编程从复杂到简洁的演变。Promise
不仅解决了“回调地狱”的问题,还提高了代码的可读性和可维护性。通过链式调用,我们可以更直观地控制异步操作的执行顺序,使得异步编程变得更加优雅和高效。随着**JavaScript**
生态的不断发展,Promise
已成为现代前端开发中不可或缺的工具,为构建复杂而高效的Web
应用提供了坚实的基础。