ES6新特性之Promise:解决异步编程的利器

322 阅读9分钟

前言

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会输出什么呢,有两种可能,一是按正常从上往下的顺序,第七行代码等待定时器执行完毕,还有一种可能是代码执行时发现这个定时器是耗时的,需要等待一秒,那就先将它放置一边,先执行后面的,最后再来执行这段代码,让我们看看结果,验证一下是那种执行方式

64.png

可以看到先执行了第七行代码,再执行了定时器里面的第五行代码,就是我们猜想的第二种方法,那为什么不用第一种方法呢,你想想看,如果后面要执行的代码和前面这个要耗时的代码没有半毛钱关系,但是按顺序来必须等这个耗时的代码先执行完毕,那其它不耗时的代码就一直被卡着,相当于一粒老鼠屎坏了一锅粥,那么这门编程语言的执行效率就太慢了,肯定就被淘汰掉了,

而第二种方法就是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呢,我们来看看输出结果

65.png

可以看到这里还是进行了异步操作,先输出了后调用的b,那如果我们想先执行a再执行b呢,那我们就可以将b的调用放入a当中

function a() {
    setTimeout(() => {
        console.log('a 执行完毕');
        b()
    }, 1000)
}
function b() {
    console.log('b 执行完毕');
}
a()

这样子的话,a不执行,b就无法执行,只有等a开始执行执行到b的调用才会开始执行b,让我们看看代码输出结果

66.png 可以看到先执行了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)

最后打印的结果

67.png 那如果最后d没有打印出来,那你知道哪里出了问题吗,你肯定不知道,那你就必须一个一个的查,这样就特别麻烦,于是官方被程序员戳脊梁骨,催促打造一个好用点的方法出来,于是官方顶不住了,在es6中更新了很多新的东西,也就是我们今天的主角Promise

2.jpg

Promise

先看下面一段异步代码

function xq() {
    setTimeout(() => {
        console.log('彭于晏相亲了');
    }, 2000)
}
function marry() {
    console.log('彭于晏结婚了');
}
xq()
marry()

这段代码会先执行marry再执行xq,下面是执行结果

68.png 那不行啊,就算是彭于晏也不能结完婚再去相亲,这不得被打断腿,为了拯救彭于晏,于是我们要将流程改一下,先相亲再结婚,除了我们刚刚讲到的回调,还可以用es6新打造的方法Promise,让我们看看怎么用的

function xq() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('彭于晏相亲了');
            resolve()
        }, 2000)
    })
}
function marry() {
    console.log('彭于晏结婚了');
}
xq().then(() =>{
    marry()
})

在彭于晏相亲的时候我们要返回es这个打造出来的新方法(也就是构造函数)创建出来的对象,构造函数中接收一个函数,我们这里写了一个箭头函数,里面的函数需要接收两个形参,这两个形参可以随便写,但是官方是resolvereject,一个是解决,一个是拒接,也比较语义化。所有我们最好按官方来。

然后我们调用xq函数返回的就是new Promise创建出来的实例对象,而这个实例对象中有then这个方法,这个方法里面也接收一个回调函数,这时候还不行,现在就要讲到那两个形参的作用,调用resolve代表函数顺利执行,reject代表执行失败,我们加上resolve(),表示成功,然后我们就可以正常使用实例对象调用then方法。我们来看看成功拯救了彭于晏吗

69.png

成功了,我们将顺序调回来了,那有人会觉得代码变复杂了,但其实复杂这一点对我们熟悉后基本没什么影响,比将一推函数作为形参传来传去好多了。

那结完婚了,小彭就该出生了,我们这样写行吗

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()
})

看看执行结果

71.png 有点太刑了,怎么小彭先出来了

3.jpg

那我们再来拯救一下彭于晏,在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()
    })
})

72.png 这时候流程就对了,我们再一次拯救了彭于晏。下面函数调用更优雅一点的写法是这样的

xq()
.then(() => {
    return marry()
})
.then(() => {
    bady()
})

将函数返回出来再调用.then,看着是不是就清晰明朗多了,这就是Promise的用法,清晰的解决了js中异步编程的问题

结语

JavaScript中,异步编程是开发过程中不可或缺的一部分。从最初的回调函数到Promise的引入,我们见证了异步编程从复杂到简洁的演变。Promise不仅解决了“回调地狱”的问题,还提高了代码的可读性和可维护性。通过链式调用,我们可以更直观地控制异步操作的执行顺序,使得异步编程变得更加优雅和高效。随着**JavaScript**生态的不断发展,Promise已成为现代前端开发中不可或缺的工具,为构建复杂而高效的Web应用提供了坚实的基础。