Promise,一种更优的异步编程统一方案
如果我们直接使用传统回调的方式去完成复杂的异步流程,就无法避免大量的回调函数嵌套,这也就会导致回调地狱问题。
为了避免回调地狱的问题,CommonJS社区率先提出了一种Promise的规范,目的就是为异步编程去提供了一种更合理,更强大的统一解决方案,后来在ES2015当中被标准化,成为语言规范。
所谓的promise其实就是一个对象,用来表示一个异步任务最终结束过后它究竟是成功,还是失败。就像是内部对外界做出了一个承诺,一开始的承若是一种待定的状态(pending),最终的结果有可能是成功(Fulfilled),也有可能失败(Rejected)
例如我承诺给你买件大衣,此使你就会等待我这个承诺的结果,也就是说我这个承诺此时是一个待定的状态,如果我确实买回来了这件大衣,那这个承诺也就是成功了(Fulfilled),那反之不管因为社什么原因我没有买回来这件大衣,那这个承诺就是失败了(Rejected)
承诺结束过后呢,不管这个承诺是成功还是失败,你都会有相对应的反应,比如成功了,我肯能会很感激,如果我失败了,你有可能把我痛骂一顿,这也就是说在承诺状态最终明确了过后,都会有相对应的任务自动被执行,而且呢这种承诺会有一个很明显的特点,一旦明确了结果之后,就不可能再发生改变了,例如我没有买到大衣,那这个给你买大衣的承诺就是失败了,他不可能再变成成功,即便说我以后再给你买了,那也是以后的事情,对于我们一开始的承诺,它还是失败的
// promise基本事例
// promise实际上就是ES2015 所提供的一个全局类集
// 构造一个promise实例(创建一个新的承诺)
// 这个构造实例 接收一个函数作为参数
const promise = new Promise(function(resolve, reject) {
// 这个函数内部可以理解为:“兑现”承诺的逻辑
// *** 这个function函数会在构造promise当中被同步执行 ***
/*
在这个函数内部,接收两个参数
分别是resolve,reject,二者都是函数
*/
// resolve的作用,是将promise这个对象的状态修改为 fulfilled(成功)
// 一般我们将异步任务的操作结果会通过resolve这个参数传递出去
// resolve(100) // 承诺达成
// reject的作用,是将promise这个对象的状态修改为rejected(失败)
// 一般失败都是传递一个错误的对象,用来表示这个承诺它为什么失败
reject(new Error('promise rejected'))
/*
注意: promise的状态一旦确定过后,就不能再被修改了
所以在这个最外层function最后只能调用二者其一
*/
})
/*
promise实例被创建完后,可以用实例的then方法分别去指定
onFulfilled 和 onRejected 的回调函数
*/
// then方法传入的第一个参数是onFulfilled回调函数(即成功之后的回调函数)
// then方法传入的第二个参数是onRejected回调函数(即失败过后的回调函数)
promise.then(function(value) {
console.log('resolve', value)
},function(error) {
console.log('reject', error)
})
/*
需要注意,即便promise内部当中没有任何的异步操作,
then方法所指定的回调函数,它仍然会进入到消息队列中排队
也就是说我们必须要等待这里同步代码全部执行完了才会执行
*/
console.log('end') // 这里会先打入console.log 再回打印出then方法里面的内容
promise使用栗子
// promise 方式的AJAX
function ajax(url) {
// url 接收外界需要请求的地址
// 直接对外返回一个promise对象,相当于对外做出一个承诺
return new Promise(function(resolve,reject) {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'json' // 相应类型
xhr.onload = function() {
if(this.status === 200) {
// 成功
resolve(this.response)
}else {
// 失败
reject(new Error(this.statusText))
}
}
xhr.send()
})
}
// 通过then方法去指定回调
ajax('/api/user.json').then(res => {
console.log('resolve', res)
},error => {
console.log('reject', error)
})
通过前面的尝试,我们发现从表象上来看,promise的本质也是使用回调函数的方式去定义异步任务结束过后所需要执行的任务,只不过这里的回调函数是通过then方法传递进去的,promise将回调分成了两种,分别是成功过后的回调,叫做onFulfilled,还有个失败的回调,叫onRejected。
如果需要连续串联执行多个异步任务,这里也就仍然会出现回调函数出现的问题
ajax('/urls.json').then(urls => {
ajax(urls.url).then(urls => {
ajax(urls.url).then(urls => {
ajax(urls.url).then(urls => {
// 形成回调地狱
})
})
})
})
此时的promise就没有任何的意义,还增加了复杂度,这种嵌套使用的方式是使用promise最常见的错误
正确的方法是,借助于promise then方法的链式调用的特点,尽量的去保证异步任务的扁平化
相比传统回调函数的方式,promise最大的优势就是可以链式调用,这样就可以最大程度的避免回调嵌套
总结下promis链式调用
正如前面所说,promise的结果一旦调用失败,就会调用在then方法去传入的第二个参数onRejected的回调函数
// promise 方式的AJAX
function ajax(url) {
// url 接收外界需要请求的地址
// 直接对外返回一个promise对象,相当于对外做出一个承诺
return new Promise(function(resolve,reject) {
// 去调用一个根本不存在的方法
// foo()
// 手动调用一个异常
throw New Error()
// 以上这些都是会被onRejected捕获到异常
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'json' // 相应类型
xhr.onload = function() {
if(this.status === 200) {
// 成功
resolve(this.response)
}else {
// 失败
reject(new Error(this.statusText))
}
}
xhr.send()
})
}
// 通过then方法去指定回调
ajax('/api/user.json').then(res => {
console.log('onFulfilled', res)
},error => {
console.log('onRejected', error)
})
所以说onRejected回调,它实际上就是为promise当中的异常去做一些处理,在promis失败了或者是出现了异常时,onRejected都会被执行
关于onRejected回调的注册,还有一个更常见的用法,就是使用promis实例的catch方法去注册onRejected回调
ajax('/api/user.json').then(res => {
console.log('onFulfilled', res)
}).catch(error => {
console.log('onRejected', error)
})
相对来说,用catch方法去指定失败回调要更为常见一些,因为这种方式会更适合于链式调用,具体原因往下看
从表象上看,用catch方法去注册失败的回调跟直接在then方法当中去注册失败的回调效果是一样的,都能够捕获到promis在执行过程中的异常,但仔细对比这两种方式有很大的差异
在promise类集当中,还有几个静态方法也经常会用到
promise.reslove(),这个方法的作用是快速的把一个值转化为一个promise对象
Promise.resolve()这个方法如果接收到的是另一个promise对象,那这个promise对象会被原样返回
除了Promise.resolve的静态方法,还有个与之对应的Promise.reject()方法,他的作用是快速创建一个一定是失败的promise对象
这里的参数就没那么多情况了,因为无论传入什么样的数据,传入的参数都会作为promise失败的理由(即失败的原因)
前面介绍的操作都是通过promise去串联执行多个异步任务,也就是一个任务结束过后再去开启下一个任务
如果需要同时去并行执行多个异步任务,promise提供了更完善的体验
promise.all方法可以将多个promise合并为一个promise统一去管理
// promise 并行执行
function ajax(url) {
// url 接收外界需要请求的地址
// 直接对外返回一个promise对象,相当于对外做出一个承诺
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'json' // 相应类型
xhr.onload = function () {
if (this.status === 200) {
// 成功
resolve(this.response)
} else {
// 失败
return reject(new Error(this.statusText))
}
}
xhr.send()
})
}
/**
* 例如在页面当中要涉及到请求多个接口的情况,
* 如果这些请求相互之间没有什么依赖,最好的选择是同时去请求,
* 这样避免一个一个依次去请求会消耗更多的时间
*/
// 并行请求很容易实现,只需要单独调用ajax多次即可
/*
ajax('/api/users.json')
ajax('/api/users.json')
但是我们怎么去判断所有的请求都已经结束了呢这样一个时机
使用promise.all 方法能实现
*/
// 使用promise.all
/**
* promise.all() 方法需要接收的是一个数组
* 数组当中的每个元素都是一个promise对象
* 可以把这些promise对象看作一个一个的异步任务
* promise.all() 方法会返回一个全新的promise对象
* 当内部所有的promise都完成过后,
* 返回的整个全新的promise才会完成
* 而此时全新的promise拿到的值是一个数组
* 数组里面包含着每个异步任务执行后的结果
*/
var promise = Promise.all([
ajax('/api/users.json'),
ajax('/api/posts.json')
])
promise.then(value => {
console.log(value) // 返回的是数组
})
/**
* 注意,使用promise.all方法里面的异步任务在执行的过程当中,
* 只有所有的任务都成功结束了,新的promise它才会成功结束
* 如果有任何其中一个任务失败,那新的promise就会以失败结束
*/
var promise2 = Promise.all([
ajax('/api/users.json'),
ajax('/api/post1111s.json') // 请求不存在的地址
])
promise.then(value => {
console.log(value) // 返回的是数组
}).catch(error => {
console.log(error)
})
除了Promise.all以外,还有Promise.race()
Promise.race() 方法同样可以把多个promise对象组合为一个全新得promise对象,但是与Promise.all 有所不同的是:
Promise.all是等待所有的任务结束过后才会结束,而Promise.race是跟着所有任务当中第一个完成的任务一起结束,也就是说只要有任何一个任务完成了,那所返回的新的promise对象就会完成
// 调用ajax 得到一个promise对象
const request = ajax('/api/uers.json')
// 单独去创建一个异常的promise对象
const timeout = new Promise((resolve,reject) => {
setTimeout(() => reject(new Error('timeout')), 500)
})
// 使用Promise.race 合并
Promise.race([
request,
timeout
]).then(value => {
console.log(value)
})
/**
* 情况有可能是
* 如果500ms 内 request请求完成了,就可以正常的得到响应结果
* 如果500ms 过后,request还没有请求完成,那么Promise.race返回的新的对象就会以
* timeout 这个失败的结果结束
*
* 因为Promise.race 就是以第一个结束的任务为准
*/
总结:把多个promise对象组合到一起的方法分别是 Promise.all() & Promise.race()
这两个方法最简单的区别是:
即便promise当中并没有任何的异步操作,他的回调函数仍然会进入到回到队列当中去排队,也就是必须要等待当前所有的同步代码都执行完了过后才会去执行promise当中的回调
// promise 的执行顺序
console.log('global start')
Promise.resolve()
.then(() => { // 即便没有异步的操作,它的回调仍然会异步调用
console.log('promise1')
})
.then(() => {
console.log('promise2')
}).then(() => {
console.log('promise3')
}).then(() => {
console.log('promise4')
})
console.log('global end')
/**
* 打印的结果
* global start
* global end
* promise1
* promise2
* promise3
* promise4
*/
回调队列中的任务称之为 “宏任务”,而宏任务执行过程中可以临时加上一些额外需求,对于临时额外的需求可以选择作为一个新的宏任务进到队列中排队(如setTimeout),也可以作为当前任务的 “微任务” (如Promise的回调)
微任务指的是,直接在当前任务结束过后立即执行
Promise的回调时作为微任务执行的,它会在本轮调用结束的末尾去自动执行,而setTimetout是以宏任务形式进入到回调函数的
微任务的目的:可以提高整体的响应能力
绝大多数异步调用都是作为宏任务执行进入到回调队列
目前Promise的回调,还有node当中的process.nextTick他们都是作为微任务直接在本轮调用的末尾就执行了
console.log('global start')
// 添加一个传统回调
setTimeout(() => {
console.log('setTimeout')
}, 0);
Promise.resolve()
.then(() => { // 即便没有异步的操作,它的回调仍然会异步调用
console.log('promise1')
})
.then(() => {
console.log('promise2')
}).then(() => {
console.log('promise3')
}).then(() => {
console.log('promise4')
})
console.log('global end')
/**
* 打印的结果
* global start
* global end
* promise1
* promise2
* promise3
* promise4
* setTimeout
*/
/**
* 为什么最后才打印出 setTimeout呢 ?
* 因为setTimetout是以宏任务形式进入到回调函数的
* Promise的回调是作为微任务执行的,它会在本轮调用结束的末尾去自动执行
*/
在ES2017标准中,新增了一个async的函数,同样提供一个扁平化的异步编程体验,async 可以给我们返回一个promise对象。
async function main () {
try {
const users = await ajax('/api/users.json')
console.log(users)
const posts = await ajax('/api/posts.json')
console.log(posts)
const urls = await ajax('/api/urls.json')
console.log(urls)
} catch (e) {
console.log(e)
}
}
const promise = main()
promise.then(() => {
console.log('all completed')
})