前言
在JavaScript的世界里,它在工作的时候就像是只有一个人在工作一样是一门单线程语言,而这样的结果导致了它其中出现了一种名为异步操作的东西。在es6之前异步操作让代码发生层层嵌套,从而产生了一种名为回调地狱这种令人望而生畏的东西,js官方也是为了我们着想创建了一个名为Promise的东西用来解决这个问题,下面我们就来学习一下如何利用Promise来解决异步问题。
1. 异步
在开头我们说到了js是一门单线程语言,而什么是单线程语言呢?我们可以简单的理解为一心不能二用,一次只能干一件事,下面我们来看一段代码演示一下什么叫单线程语言:
let a = 1
console.log(a);
setTimeout(() => {
a = 2
console.log(a);
}, 1000);
console.log(a);
// 输出:
// 1
// 1
// 2
由于js是一门单线程语言所以我们可以简单的理解为代码只能一行一行的执行,不能同时执行多个代码,别的编程语言比如Java就是多线程语言可以一心二用,同时干很多事情。在我们简单了解完了单线程和多线程之后,下面我们来看看上面代码为何会先执行后面的代码,而不执行setTimeout中的代码。
关于这个这个问题呢就是我们要了解的异步操作和同步操作了,在我们本来的认知中上面的代码执行过程应该是从上到下依次执行的,但是我们却把那个需要耗时的代码放到最后执行,而这个就是异步操作。
异步操作:可以同时执行多条代码,不需要等待前一条代码执行完成后才执行下一条代码。这种方式可以避免程序阻塞,提高程序的性能和响应速度。
同步操作:一次执行一条代码,然后等待这条代码执行完成后再执行下一条代码。这种方式会导致程序阻塞,直到前一条代码执行完成后,程序才可以继续执行下一条代码。
大家看到上面这两条概念可能有点模糊,下面我们来用大白话给大家讲讲什么是同步什么是异步。
我们上学的时候每天需要买早餐是吧,咱们胃口比较大早上呢我就是想吃炒粉和包子,在买早餐的时候呢有两个不同的同学一个是头比较铁的,一个呢就是咱比较灵活一点的。在买炒粉的时候我们需要时间等待吧,那么我们比较聪明在这个时候我们可以利用等待的时间去买包子,这样就可以节省时间是吧。而另一个头铁的同学呢就想着我必须先等他炒完炒粉再去买包子,这样的话他所花费的时间肯定比我们更长。而咱比较聪明的呢就是异步操作,头比较铁的呢就是同步操作。
由此可见上面代码执行的就是异步操作,那么同步操作听起来好像挺der啊为啥还要有他呢?正所谓咱不会留没用的东西,就跟咱身上的阑尾一样之前都说没用,但是最近不还是说有用吗,下面咱就来聊聊同步操作和回调函数的关系。
异步
- js是单线程语言,一次性只能干一件事(多线程执行效率高但是开销性能也多,开销成本也高)
- js 遇到需要耗时执行的代码会将其先挂起,等到后续不耗时的代码执行完毕后再执行耗时代码。(就类似于考试碰到不会的先跳过)
2. 解决异步
前面我们不是说来聊聊同步操作和回调函数吗,怎么讲起来解决异步了?解决异步操作就跟我们想让代码执行同步操作和回调函数有关,下面我们来看一个场景:
当我们在点开网页的时候,网页是不是得先给我们返回网页中的信息,然后才能将网页填充好展现给我们。而网页返回后台信息给我们是需要时间的,我们需要等待返回信息给我们之后,才能展现,这个过程中如果我们是js那么就会执行异步操作这样的话就不是我们想要的结果了,这时候我们就会想让它进行同步操作了,下面我们来看看如何解决异步操作。
2.1 回调函数
下面我们来看一段js异步操作的代码:
function a() {
setTimeout(() => {
console.log('a 执行完毕');
}, 1000)
}
function b() {
console.log('b 执行完毕');
}
a()
b()
// 输出:
// b 执行完毕
// a 执行完毕
我们可以看到js会执行异步操作先执行b然后执行a,那么大家可以想想我们有没有办法可以将它变成同步操作的代码,先执行a,然后再执行b,下面我们来实现一下:
function a(cb) {
setTimeout(() => {
console.log('a 执行完毕');
cb()//本来是b调用完了a调用,现在是a调用完了才b调用这个就是回调
}, 1000)
}
function b() {
console.log('b 执行完毕');
}
a(b)
b()
// 输出:
// a 执行完毕
// b 执行完毕
根据下面修改后的代码我们知道,只需要将b调用放在a中的setTimeout中,等它执行完了然后再执行即可实现这个效果。而这个cb()就是我们前面所说的回调函数,通过使用这种方法可以用来解决异步问题。
回调函数:它作为参数传递给另一个函数,并在特定条件下被调用,通常用于处理异步操作的结果。
2.2 回调地狱
在我们简单了解完了回调函数之后,下面我们来看看回调地狱是个什么玩意,有没有这么吓人。
下面来看一段代码展示:
function a() {
setTimeout(() => {
console.log('a 执行完毕');
}, 1000)
}
function b() {
setTimeout(() => {
console.log('b 执行完毕');
}, 1500)
}
function c() {
setTimeout(() => {
console.log('c 执行完毕');
}, 500)
}
function d() {
console.log('d 执行完毕');
}
我们可以看到上面代码一共有三个需要消耗时间的代码,那么如果我们想要把上面代码的执行顺序变成a() -> b() -> c() -> d()的话运用回调函数该如何实现,下面我们来看看实现效果:
function a(cb, cb2, cb3) {
setTimeout(() => {
console.log('a 执行完毕');
cb(cb2, cb3)
}, 1000)
}
function b(cb, cb3) {
setTimeout(() => {
console.log('b 执行完毕');
cb(cb3)
}, 1500)
}
function c(cb) {
setTimeout(() => {
console.log('c 执行完毕');
cb()
}, 500)
}
function d() {
console.log('d 执行完毕');
}
a(b, c, d)
看到这段代码有同学可能就两眼一黑了说这是什么玩意,到这里套娃呢?看到这个三层嵌套的其实还好,大家以后工作的话可能还会有比这多得多的嵌套,那到时候肯定是跟看天书一样,而这种回调函数的多层嵌套就是我们所说的回调地狱,这个通常也是用来解决异步操作的。
回调——回调地狱(代码嵌套过深,维护成本大,一旦出现问题很难排查):回调函数嵌套多层过深,如果其中一个函数执行失败,会一直回调到最外层,导致整个函数执行失败。
2.3 Promise
在es6之前呢,程序员们一直使用的都是这种方法,后来官方终于受不了被戳脊梁骨了为我们打造了一个Promise可以用来解决这个回调地狱的问题,下面我们就来看看如何使用Promise。
首先呢我们来看一段异步的代码:
function xq() {
setTimeout(() => {
console.log('小明相亲了');
}, 2000)
}
function marry() {
console.log('小明结婚了');
}
xq()
marry()
// 输出:
// 小明结婚了
// 小明相亲了
我们看到上面的输出结果后会发现,这不是乱了套吗,怎么能先结婚再相亲呢,我们正常不都是先相亲再结婚吗,下面我们来教育一下不懂事的小明:
function xq() {
//resolve是执行成功的意思,reject是执行失败的意思
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('小明相亲了');
resolve('相亲成功了')
}, 2000)
})
}
function marry() {
console.log('小明结婚了');
}
xq().then((res) => {
console.log(res)
marry()
})
// 输出:
// 小明相亲了
// 相亲成功了
// 小明结婚了
在这个问题上同样可以使用回调函数大家可以自行尝试,但是我们为了更优雅一点还是选择Promise毕竟新东西总得尝试尝试。
我们可以看到如果我们使用Promise的话不需要使用回调函数,只要将需要进行修改的异步操作地方返回一个Promise的实例对象,并且传入一个回调函数,其中有两个参数:
Promise两个参数
- resolve:执行成功然后执行
then()操作,可以传入参数会返回给then中的回调函数,得放在这种需要时间的代码部分,不写在其中then不会执行。- reject:执行失败然后执行
catch()操作,抛出错误
.then()会返回一个状态是准备的Promise对象,既不成功也不失败
然后我们在then中传入一个回调函数,并且用res接收前面resolve传入过来的参数,输出后调用marry(),使用Promise的过程我们用咱自己的话来说:
咱如果想让它按顺序执行就是让他听话,我们先给它一个承诺
Promise如果下定了决心resolve实现这个承诺,然后then就能让它就能明白你的决心res按照你接下来想做的事情执行。如果没有下定决心来实现这个承诺它就得拒绝
reject,然后就它就不会帮你干这事情并且让你认识到这个错误catch。
多层回调
上面这段代码只是简单的解决了一个回调,那如果有多个回调乃至回调地狱我们该如何解决呢,下面我们来看一个小场景并且用代码实现。
我们的小明呢结婚了之后想要个小小明,但是这也得结婚之后是吧,下面我们来实现一下:
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 baby() {
console.log('小小明出生了');
}
xq().then((res) => {
console.log(res);
marry().then(() => {
baby()
})
})
我们可以看到执行结果的确是按照我们的计划进行,下面我们来讲解一下过程中的多层调用。我们可以看到在第一层中我们在调用完了
xq之后使用then进行接下来的marry,然后在marry中再次使用了Promise,然后在marry执行完后再执行then中的baby。如果有多个的话我们就可以一直这样嵌套下去,这样的话就提高了代码的复用性,如果有修改函数调用就不用去函数体中修改了。这时候就有同学会问了,有没有更优雅的方法,下面我们来看看使用Promise实现的另一种样子:
//前面三个函数还是一样的,下面我们来对then部分进行修改
xq()
.then((res) => {
console.log(res);
return marry()//返回的是Promise实例,所以可以继续then
})
.then((res) => {
console.log(res);
baby()
})
我们可以看到上面代码不会像前面一样写成了一坨,而是可以拆解开来,由于marry()返回的是一个Promise实例对象,所以我们可以对其接着用then进行调用,所以以后在写代码时如果碰到了多重嵌套的话就可以使用这种方法方便我们查看嵌套。
reject
在我们前面看完了Promise中的resolve参数后,接下来我们来看看它后面的这个reject参数,在前文中我们说到如果使用这个方法则不会进行then而是进行catch,下面来看看代码实现:
function a() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('a');
// resolve('a 执行完毕')// .then会执行
reject('a 执行失败')// .catch会执行
}, 1000)
})
}
a()
.then(res => {
console.log(res);
})
.catch(err => {
console.log(err);
})//用来捕获reject抛出的错误
// 输出:
// a
// a 执行失败
我们可以看到如果使用了reject的话then并不会执行,而是执行后面的catch,在这里大家就要注意平常到底是使用resolve还是reject了。
Promise实战示例
我们在了解完了Promise的基本使用方法之后,下面我们来看看它的实战效果,当我们想从一个后端拿到数据时,这是需要时间的,我们只有等到从后端拿到数据之后才能在页面展示,下面我们来完成一下这个需求:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul id="ul"></ul>
<script>
function getData() {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest()//帮我们创建一个http请求体实例对象
xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true)
xhr.send()// 发送请求
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(JSON.parse(xhr.responseText));// 解析成功后获得的数据
resolve(JSON.parse(xhr.responseText).movieList)// 拿到数据并且返回到then中赋值给res
}
}// 监听状态变更
})
}
function showList(data) {
data.forEach(item => {
let li = document.createElement('li')
li.innerText = item.nm
document.getElementById('ul').appendChild(li)
});
// console.log(`拿到了数据${data}`);
}
getData().then((res) => { showList(res) })//成功拿到数据后然后执行showList进行调用在页面生成数据
</script>
</body>
</html>
以上呢就是我们通过使用Promise实现的一个从后端拿到数据的一个效果展示,大家可以自行理解一下代码,主要就是返回一个Promise示例对象,然后得注意resolve放的位置,它必须放在需要时间的位置。当我们执行完了这个获得数据的代码之后再执行showList进行页面渲染数据。
结语
以上就是关于解决异步的一些方法以及Promise的基础使用,日后呢会继续更新有关Promise的相关知识,感谢各位的观看。