本章主要讲解关于Promise汇总性的介绍,其中顺便也扩展了一些涉及到的知识。
为什么需要Promise
?
任何一个事物的产生都定有它存在的意义,所以我们需要清楚Promise为什么出现,它解决了什么问题。
回调嵌套
在Promise未诞生以前,我们通过回调表达程序异步和管理并发,当然现在一些老的项目为了保证兼容性仍在使用。回调是JavaScript中实现异步最简单的方式,你可以将回调理解为程序的延续,即在当前同步代码执行完毕以后才会在未来某个时间执行。当我们的回调需要用到上一个回调的结果时,就产生了嵌套,类似下面这样:
ajax1(null, function (value1) {
ajax2(value1, function(value2) {
ajax3(value2, function(value3) {
ajax4(value3, function(value4) {
// Do something with value4
});
});
});
});
嵌套的层级多了,就产生了所谓的回调地狱
,也称毁灭金字塔
。但实际情况远比上面展现的要复杂得多,这使得追踪代码执行顺序的难度成倍增加。你也一定遇到过结尾少写})
,排查大半天的情况,这就是因为代码太复杂了。
回调地狱所表现出来的问题,归纳下面几点:
- 难以追踪代码的执行顺序
- 代码复杂且极其脆弱
- 程序若出现执行顺序偏离的异常情况,结果难以预测(前面的回调会阻塞后面代码的执行,若前面失败,后面都不会执行)
信任问题
就是多个异步调用的时候缓和其他三方的接口,如果三方的接口出问题或者改了什么造成回调失败
我们经常使用第三方的 API
(如Ajax
),在将回调交付给第三方时,就意味着将控制权转交给了第三方
,这叫控制反转
。我们的回调由第三方负责调用,但对于第三方,我们能充分信任吗?
这里列举一些情况:
- 调用回调过早
- 调用回调过晚
- 调用回调的次数过多
- 回调未调用
- 未能传递参数/环境值
- 吞掉可能出现的错误或异常
对于这些情况,我们可能都要在回调函数中做些处理,并且是每次执行回调函数的时候都要做这些相同的处理,这就带来了很多重复的代码。
所以Promise
的出现致力于解决这两大主要问题。
Promise
是如何解决上述问题的?
回调嵌套
Promise
对于回调而言,也只是改变了回调的位置,让代码看上去更有顺序性。
在Promise
对象的原型链上定义了then
方法,调用then
方法会自动创建一个新的 Promise
从调用返回,从而支持链式调用。链式调用的目的就是,让程序顺序执行,解决回调嵌套,以更符合我们人类大脑顺序思考的方式。上面的例子,我们使用Promise
后如下:
// request 方法
function request(param){
return new Promise(function(resolve, reject){
ajax(param, function(val){
resolve(val)
})
})
}
// 链式调用,顺序执行(1,2,3,4为了表明顺序)
request1(null)
.then(value1 => request2(value1))
.then(value2 => request3(value2))
.then(value3 => request4(value3))
Promise的含义
Promise
是异步编程的一种解决方案,ES6将其写进了语言标准。所谓Promise
就是一个容器,里面保存着未来才会结束的事件(通常是一个异步操作)的结果。
Promise
对象有以下两个特点:
- 对象的状态不受外界影响。
Promise
对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled/resolved
(已成功)、rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。 - 一旦状态改变,就不会再改变,任何时候都可以得到这个结果。
Promise
对象的状态改变,只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为resolved
(已定型)。如果改变已经发生了,你再对Promise
对象添加回调函数,也会立即得到这个结果。
Promise
的优点:
可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。
基本用法
ES6 规定,Promise
对象是一个构造函数,用来生成Promise
实例。
const promise = new Promise((resolve,reject)=>{
//此处执行一些异步操作(调用后台API,定时器等)
if(/*异步操作成功*/){
resolve(value);
}else{
reject(error)
}
})
//其中两个函数的参数值分别为成功和失败后想要传递的结果
Promise
构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。它们是两个函数,由 JavaScript
引擎提供,不用自己部署。
resolve
函数的作用是,将Promise
对象的状态从“未完成”变为“成功”(即从 pending
变为 resolved
),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject
函数的作用是,将Promise
对象的状态从“未完成”变为“失败”(即从 pending
变为 rejected
),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
then
方法可以接受两个回调函数作为参数。第一个回调函数是Promise
对象的状态变为resolved
时调用,第二个回调函数是Promise
对象的状态变为rejected
时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise
对象传出的值作为参数。
promise.then(res=>{
//对于成功回调接受的数据做处理
},err=>{
//对于失败的回调数据做处理
})
注:Promise
新建后就会立即执行。
Promise.prototype.then()
Promise
实例具有then
方法,也就是说,then
方法是定义在原型对象上Promise.prototype
上的,它的作用是为 Promise
实例添加状态改变时的回调函数。前面说过,then
方法的第一个参数是resolved
状态的回调函数,第二个参数(可选)是rejected
状态的回调函数。
then
方法返回的是一个新的Promise
实例(注意,不是原来那个Promise
实例)。因此可以采用链式写法,即then
方法后面再调用另一个then
方法。第一个回调函数完成以后,会将返回结果作为参数,春如第二个回调函数。
采用链式的 then
,可以指定一组按照次序调用的回调函数。(ES7中的async/await
)也可以实现链式调用,除此之外,Promise
的all
方法可以实现并行执行。
了解了基础的 Promise
和 then
之后,我们便可以自己创建一个 Promise
。
Promise.all() 批量执行
Promise.all([p1, p2, p3])用于将多个promise实例,包装成一个新的Promise实例,返回的实例就是普通的promise
它接收一个数组作为参数
数组里可以是Promise对象,也可以是别的值,只有Promise会等待状态改变
当所有的子Promise都完成,该Promise完成,返回值是全部值得数组
有任何一个失败,该Promise失败,返回值是第一个失败的子Promise结果
//切菜
function cutUp(){
console.log('开始切菜。');
var p = new Promise(function(resolve, reject){ //做一些异步操作
setTimeout(function(){
console.log('切菜完毕!');
resolve('切好的菜');
}, 1000);
});
return p;
}
//烧水
function boil(){
console.log('开始烧水。');
var p = new Promise(function(resolve, reject){ //做一些异步操作
setTimeout(function(){
console.log('烧水完毕!');
resolve('烧好的水');
}, 1000);
});
return p;
}
Promise.all([cutUp(), boil()])
.then((result) => {
console.log('准备工作完毕');
console.log(result);
})
Promise.race() 类似于Promise.all() ,区别在于它有任意一个完成就算完成
let p1 = new Promise(resolve => {
setTimeout(() => {
resolve('I\`m p1 ')
}, 1000)
});
let p2 = new Promise(resolve => {
setTimeout(() => {
resolve('I\`m p2 ')
}, 2000)
});
Promise.race([p1, p2])
.then(value => {
console.log(value)
})
手写Promise
首先写 Promise
构造函数,由以上Promise
的使用可知,其参数为一个函数,又被称为执行器函数(executor),并且执行器函数会被立即调用,执行器函数也会接收两个参数,且这两个参数均为函数。
function Promise(executor) {
executor(resolve, reject);
}
Promise
最重要的方法就是then
方法,因此为了能够让实例调用这个方法,我们必须将这个方法写在其原型链上,并且它接受两个参数,一个为成功的回调,一个为失败得回调。
Promise.prototype.then=function(onResolved,onRejected){
}
function Promise(executor) {
let self=this; //保留this。防止后面方法出现this只想不明的问题
self.status='pending'; //promise的默认状态是pending
function resolve(){
self.status='resolved'; //成功函数将其状态修改为resolved
}
function reject(){
self.status='rejected'; //失败函数将其函数修改为rejected
}
executor(resolve, reject);
}
为了保证 Promise
实例状态一旦变更不能再次改变,需要进行判断
function Promise(executor) {
let self = this; //保留this。防止后面方法出现this只想不明的问题
self.status = 'pending'; //promise的默认状态是pending
self.success = undefined; //保存成功回调传递的值
self.error = undefined; //保存失败回调传递的值
function resolve() {
if (self.status === 'pending') {
self.status = 'resolved'; //成功函数将其状态修改为resolved
}
}
function reject() {
if (self.status === 'pending') {
self.status = 'rejected'; //失败函数将其函数修改为rejected
}
}
executor(resolve, reject);
}
之后需要将调用之后的成功或失败的结果保存起来
function Promise(executor) {
let self = this; //保留this。防止后面方法出现this只想不明的问题
self.status = 'pending'; //promise的默认状态是pending
self.success = undefined; //保存成功回调传递的值
self.error = undefined; //保存失败回调传递的值
function resolve(success) {
if (self.status === 'pending') {
self.status = 'resolved'; //成功函数将其状态修改为resolved
self.success=success; //将成功的值保存起来
}
}
function reject(error) {
if (self.status === 'pending') {
self.status = 'rejected'; //失败函数将其函数修改为rejected
self.error=error; //将失败的值保存起来
}
}
executor(resolve, reject);
}