想要学习
Promise
,我们首先要了解异步编程
、回调函数
、回调地狱
三方面知识:
异步编程技术使你的程序可以在执行一个可能长期运行的任务的同时继续对其他事件做出反应而不必等待任务完成。
与此同时,你的程序也将在任务完成后显示结果。
假设现在老板让你修改一个很紧急并且很重要的代码,让你下班前必须改完。并且为了督促进度,老板搬了个椅子坐在一边盯着你敲。
你心里肯定已经犯嘀咕:“你有这么闲吗?就不能去干点其他事情吗?”
老板仿佛接收到了你的心电图一样:“我就在这等着,你改完代码之前我哪也不去。”
这个例子中老板交给你任务后就一直等待什么都不做直到你改完,这个场景就是所谓的同步。
第二天,老板又交给了你一项任务。
不过这次就没那么着急啦,这次老板轻描淡写“今天的这个代码不着急,你写完告诉我一声就行。”
这次老板没有盯着你写代码而是转身刷视频去了,你写完后简单的和老板报告了一声“我写完啦!”
这个例子老板交代完任务就去忙其它事情,你完成任务后简单的告诉老板任务完成,这就是所谓的异步。
值得注意的是:在异步这种场景下你在改代码的同时老板在刷视频,这两件事在同时进行,因此这就是异步比同步高效的本质。
与异步任务相对应的概念是同步任务,同步任务在主线程上排队执行,只有前一个任务执行完毕,才能执行下一个任务。异步任务不进入主线程,而是进入异步队列,前一个任务是否执行完毕不影响下一个任务的执行。这里拿定时器作为异步任务举例:
// setTimeout中的内容不会先被输出,而是先输出异步任务之后的内容
setTimeout(() => {
console.log('我在定时器里捏!!')
}, 2000)
console.log('我在定时器后捏~~')
如果按照代码编写的顺序,应该先输出我在定时器里捏!!
,再输出我在定时器后捏~~
。但实际输出为:
这种不阻塞后面任务执行的任务就叫做异步任务。
把一个函数当作参数传递给另一个函数,但是此函数并不会立即执行,而是在将来特定的时机再去调用,这个函数就叫做回调函数。在定时器
setTimeout
以及Ajax
的请求时都会用到回调函数。
再举个栗子:
你到一个商店去买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。
在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。
回调函数在我们启动一个异步任务的时候就会告诉它:等你完成了这个任务之后要干什么。这样一来主线程几乎不用关心异步任务的状态了,他自己会善始善终。
// setTimeout中第一个参数就是回调函数,只有在1秒后执行
setTimeout(() => {
console.log('执行回调函数!!')
}, 1000);
console.log('先执行我捏~~')
根据前面对异步任务的介绍,应该知道此代码会先执行定时器后的语句,再执行定时器及回调函数。
根据前面对异步编程以及回调函数的介绍我们可以得出一个结论:存在异步任务的代码,不能保证其按照顺序执行,那如果我们非要代码顺序执行呢?
比如我要间隔不同时间输出三句话,语序必须是下面这样的:我在定时器1里捏!!
,我在定时器2里捏!!
,我在定时器3里捏!!
setTimeout(() => {
console.log('我在定时器1里捏!!')
}, 3000)
setTimeout(() => {
console.log('我在定时器2里捏!!')
}, 2000)
setTimeout(() => {
console.log('我在定时器3里捏!!')
}, 1000)
console.log('我在定时器后捏~~')
当使用定时器顺序调用时,则会出现输出顺序错乱的问题:
所以必须要这样操作,才能保证输出顺序正确:
setTimeout(() => {
console.log('我在定时器1里捏!!')
setTimeout(() => {
console.log('我在定时器2里捏!!')
setTimeout(() => {
console.log('我在定时器3里捏!!')
}, 1000)
}, 2000)
}, 3000)
console.log('我在定时器后捏~~')
可以看到,代码中的回调函数层层嵌套,并且嵌套了3层,这种回调函数中嵌套回调函数的情况就叫做回调地狱。
所以回调地狱就是为实现代码顺序执行而出现的一种操作,它会造成我们的代码可读性非常差,后期不好维护。
那么该如何解决回调地狱问题呢?
Promise,中文翻译过来就是承诺
,意思是承诺在未来某一个时间点返回数据给你。它是JS中的一个原生对象,是一种异步编程的解决方案,可以替换掉传统的回调函数解决方案。
首先,Promise 对象有三个状态:pending
(进行中),fulfilled
(已成功),rejected
(已失败)
其次,Promise 构造函数接收一个函数作为参数,该函数是同步的并且会被立即执行,所以我们称之为起始函数,我们需要处理的异步任务就卸载在该函数体内,该函数的两个参数是resolve
,reject
。异步任务执行成功时调用resolve
函数并传递成功的结果,反之调用reject
并传递失败的原因。
最后,Promise 构造函数返回一个 Promise 对象,该对象具有以下几个方法:
then
:用于处理 Promise 成功
状态的回调函数。catch
:用于处理 Promise 失败
状态的回调函数(有任何异常都会直接执行)。finally
:无论 Promise 是成功还是失败,都会执行的回调函数。Promise 本身只是一个容器,真正异步的是它的两个回调resolve
和reject
,分别表示 Promise 成功
和失败
的状态。其本质不是控制异步代码的执行顺序 ,而是控制异步代码结果处理的顺序。
那么如何改变 Promise 的状态:
pending
就会变为 fulfilled
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('用户数据读取成功!!')
}, 1000)
})
p.then(value => {
console.log(value)
}).catch(reason => {
console.log(reason)
})
console.log(p)
pending
就会变为 rejected
const p = new Promise((resolve, reject) => {
setTimeout(() => {
reject('用户数据读取失败~~')
}, 1000)
})
p.then(value => {
console.log(value)
}).catch(reason => {
console.log(reason)
})
console.log(p)
pending
就会变为 rejected
const p = new Promise((resolve, reject) => {
throw new Error('出错啦!!')
})
console.log(p)
注意:一旦从进行状态变成为其他状态就永远不能更改状态了。
Promise 链式编程可以保证代码的执行顺序,前提是每一次在than
做完处理后,一定要 return 一个 Promise对象,这样才能在下一次then
时接收到数据。
在对 Promise 有了一定了解之后,再尝试通过 Promise 链式调用来解决上文介绍回调地狱时所提出的问题,实现以下语句:我在定时器1里捏!!
,我在定时器2里捏!!
,我在定时器3里捏!!
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('我在定时器1里捏!!')
}, 3000);
}).then(value => {
console.log(value)
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('我在定时器2里捏!!')
}, 2000);
})
}).then(value => {
console.log(value)
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('我在定时器3里捏!!')
}, 1000);
})
}).then(value =>
console.log(value)
).catch(reason =>
console.log(reason))
上述代码看上去很凌乱,可读性并不好,所以我们可以将它的核心部分写成一个 promise 函数:
function promise(value, time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(value)
}, time)
})
}
promise('我在定时器1里捏!!', 3000)
.then(data => {
console.log(data);
return promise('我在定时器2里捏!!', 2000);
})
.then(data => {
console.log(data);
return promise('我在定时器3里捏!!', 1000)
})
.then(data => {
console.log(data);
})
.catch(data => {
console.log(data);
})
Promise 虽然摆脱了回调地狱,但是then
的链式调用也会带来额外的阅读负担,并且 Promise 传递中间值非常麻烦。
同时, Promise 的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个then
代码块中使用调试器的步进step-over
功能,调试器并不会进入后续的then
代码块,因为调试器只能跟踪同步代码的每⼀步。
所以ES2017推出了新的语法 async/await
来更好的解决异步问题,下一篇文章会给大家带来async/await
的相关介绍,敬请期待~