前言
不得不说, promise 这玩意,是每个面试官都会问的问题,但是你真的了解promise吗?其实我也不了解,下面的内容都是我从掘金、知乎、《ECMAScript6入门》上看的博客文章等资料,然后总结的,毕竟自己写一遍,更有助于理解,如有错误,请指出 ~
什么是回调地狱 ?
在过去写异步代码都要靠回调函数,当异步操作依赖于其他异步操作的返回值时,会出现一种现象,被程序员称为 “回调地狱”,比如这样 :
// 假设我们要请求用户数据信息,它接收两个回调,假设我们要请求用户数据信息,它接收两个回调,successCallback 和 errCallback
function getUserInfo (successCallback, errCallback) {
$.ajax({
url : 'xxx',
method : 'get',
data : {
user_id : '123'
},
success : function(res) {
successCallback(res) // 请求成功,执行successCallback()回调
},
error : function(err) {
errCallback(err) // 请求失败,执行errCallback()回调
}
})
}
骗我 ? 这哪里复杂了,明明很简单啊,说好的回调地狱呢 ? 不急,继续看
假设我们拿到了用户信息,但是我们还要拿到该用户的聊天列表,然后再拿到跟某一“陌生”男人的聊天记录呢 ?
// getUserInfo -> getConnectList -> getOneManConnect()
getUserInfo((res)=>{
getConnectList(res.user_id, (list)=>{
getOneManConnect(list.one_man_id, (message)=>{
console.log('这是我和某位老男人的聊天记录')
}, (msg_err)=>{
console.log('获取详情失败,别污蔑我,我不跟老男人聊天')
})
}, (list_err)=>{
console.log('获取列表失败,我都不跟别人聊天')
})
}, (user_err)=>{
console.log('获取用户个人信息失败')
})
大兄弟,刺激不,三层嵌套,再多来几个嵌套,就是 “回调地狱” 了。这时候,promise来了。
Promise 简介
阮一峰老师的《ECMAScript 6入门》里对promise的含义是 : Promise 是异步编程的一种解决方案,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
简单来说,Promise就是对异步的执行结果的描述对象。
状态
- pending (进行中)
- fulfilled (已成功)
- rejected (已失败)
1 : 只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
2 : 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
3 : Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected
知乎形象例子来说明promise
// 定外卖就是一个Promise,Promise的意思就是承诺
// 我们定完外卖,饭不会立即到我们手中
// 这时候我们和商家就要达成一个承诺
// 在未来,不管饭是做好了还是烧糊了,都会给我们一个答复
function 定外卖(){
// Promise 接受两个参数
// resolve: 异步事件成功时调用(菜烧好了)
// reject: 异步事件失败时调用(菜烧糊了)
return new Promise((resolve, reject) => {
let result = 做饭()
// 下面商家给出承诺,不管烧没烧好,都会告诉你
if (result == '菜烧好了')
// 商家给出了反馈
resolve('我们的外卖正在给您派送了')
else
reject('不好意思,我们菜烧糊了,您再等一会')
})
}
// 商家厨房做饭,模拟概率事件
function 做饭() {
return Math.random() > 0.5 ? '菜烧好了' : '菜烧糊了'
}
// 你在家上饿了么定外卖
// 有一半的概率会把你的饭烧糊了
// 好在有承诺,他还是会告诉你
定外卖()
// 菜烧好执行,返回'我们的外卖正在给您派送了'
.then(res => console.log(res))
// 菜烧糊了执行,返回'不好意思,我们菜烧糊了,您再等一会'
.catch(res => console.log(res))
基本用法
Promise 对象是一个构造函数,用来生成一个Promise实例。
Promise构造函数接受一个函数作为参数,这个函数有两个参数,分别是resolve()和reject()。
resovle()函数是将Promise对象从pending变成fulfilled,在异步操作完成时执行,将异步结果,作为参数传递出去。
reject()函数是将Promise对象从pending变成rejected,在异步执行失败时执行,将报错信息,作为参数传递出去。
// 简单的一个promise实例, 来自阮一峰老师的es6 示例代码
const promise = new Promise((resolve, reject) => {
// some code
if(/* 异步执行成功 */) {
resolve(res)
} else {
reject(error)
}
})
then方法
Promise 有个.then()方法,then 方法中的回调在微任务队列中执行,支持传入两个参数,一个是成功的回调,一个是失败的回调,在 Promise 中调用了 resolve 方法,就会在 then 中执行成功的回调,调用了 reject 方法,就会在 then 中执行失败的回调,成功的回调和失败的回调只能执行一个,resolve 和 reject 方法调用时传入的参数会传递给 then 方法中对应的回调函数。
// 执行 resolve
let promise = new Promise((resolve, reject) => {
console.log(1)
resolve(3)
})
console.log(2)
promise.then((data)=>{
console.log(data)
}, (err)=>{
console.log(err)
})
// 1
// 2
// 3
// 执行 reject
let promise = new Promise((resolve, reject) => {
console.log(1)
reject()
})
promise.then(()=>{
console.log(2)
}, ()=>{
console.log(3)
})
// 1
// 3
then方法
[ 注意 : then方法中的回调是异步的!!!]
为什么上面第一个示例代码的结果是 1 -> 2 -> 3呢 ?传入Promise 中的执行函数是立即执行完的啊,为什么不是立即执行 then 中的回调呢?因为then 中的回调是异步执行,表示该回调是插入事件队列末尾,在当前的同步任务结束之后,下次事件循环开始时执行队列中的任务。
Promise 的回调函数不是正常的异步任务,而是微任务(microtask)。它们的区别在于,正常任务追加到下一轮事件循环,微任务追加到本轮事件循环。这意味着,微任务的执行时间一定早于正常任务
then方法的返回值是一个新的GPromise对象,这就是为什么promise能够进行链式操作的原因。
then方法中的一个难点就是处理异步,通过setInterval来监听GPromise对象的状态改变,一旦改变,就是执行GPromise对应的then方法中相应的回调函数。这样回调函数就能够插入事件队列末尾,异步执行。
then有两个参数 : onFulfilled 和 onRejected
· 当状态state为fulfilled,则执行onFulfilled,传入this.value。当状态state为rejected,则执行onRejected,传入this.reason
· onFulfilled,onRejected如果他们是函数,则必须分别在fulfilled,rejected后被调用,value或reason依次作为他们的第一个参数
class Promise{
constructor(executor){...}
// then 方法 有两个参数onFulfilled onRejected
then(onFulfilled,onRejected) {
// 状态为fulfilled,执行onFulfilled,传入成功的值
if (this.state === 'fulfilled') {
onFulfilled(this.value);
};
// 状态为rejected,执行onRejected,传入失败的原因
if (this.state === 'rejected') {
onRejected(this.reason);
};
}
}
Promise的链式调用
由于promise每次调用then方法就会返回一个新的promise对象,如果该then方法中执行的回调函数有返回值,那么这个返回值就会作为下一个promise实例的then方法回调的参数,如果 then 方法的返回值是一个 Promise 实例,那就返回一个新的 Promise 实例,将 then 返回的 Promise 实例执行后的结果作为返回 Promise 实例回调的参数。
还记得刚开头说的那个“陌生”男人例子吗 ?这里我们用promise的链式操作重写下
// 原来的代码
getUserInfo((res)=>{
getConnectList(res.user_id, (list)=>{
getOneManConnect(list.one_man_id, (message)=>{
console.log('这是我和某位老男人的聊天记录')
}, (msg_err)=>{
console.log('获取详情失败,别污蔑我,我不跟老男人聊天')
})
}, (list_err)=>{
console.log('获取列表失败,我都不跟别人聊天')
})
}, (user_err)=>{
console.log('获取用户个人信息失败')
})
// Promise重写的代码
function handleAjax (params) {
return new Promise((resolve, reject)=>{
$.ajax({
url : params.url,
type : params.type || 'get',
data : params.data || '',
success : function(data) {
resolve(data)
},
error : function(error) {
reject(error)
}
})
})
}
const promise = handleAjax({
url : 'xxxx/user'
});
promise.then((data1)=>{
console.log('获取个人信息成功') // 获取个人信息成功
return handleAjax({
url : 'xxxx/user/connectlist',
data : data1.user_id
});
})
.then((data2)=>{
console.log('获得聊天列表')
return handleAjax({
url : 'xxxx/user/connectlist/one_man',
data : data2.one_man_id
});
})
.then((data3)=>{
console.log('获得跟某男人的聊天')
})
.catch((err)=>{
console.log(err)
})
来自ES6的 Promise.prototype.then()
Promise 实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用
来自ES6的 Promise.prototype.catch()
Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。Promise对象状态变为resolved,则会调用then方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch方法指定的回调函数,处理这个错误。另外,then方法指定的回调函数,如果运行中抛出错误,也会被catch方法捕获。
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获
一般来说,不要在then方法里面定义 reject 状态的回调函数(即then的第二个参数),总是使用catch方法。
来自ES6的 Promise.all()
Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3])
Promise.all方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。
p的状态由p1、p2、p3决定,分成两种情况。
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
来自ES6 的Promise.race()
Promise.race方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3])
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数
Promise.race方法的参数与Promise.all方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。
来自ES6 的Promise.resolve()
有时需要将现有对象转为 Promise 对象,Promise.resolve方法就起到这个作用
Promise.resolve('test')
// 等价于
new Promise(resolve => resolve('test'))
// 更多请看阮一峰老师的ES6 Promise对象
来自ES6 的Promise.reject()
Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (err) {
console.log(err) // 出错了
});
// 更多请看阮一峰老师的ES6 Promise对象
相关链接
个人博客 : https://github.com/PDKSophia/blog.io
个人掘金 : https://juejin.im/user/594ca8a35188250d892f4139
阮一峰 ES6 : http://es6.ruanyifeng.com/#docs/promise
知乎例子 : https://zhuanlan.zhihu.com/p/29632791
掘金 卡姆爱卡姆 : https://juejin.im/post/5b2f02cd5188252b937548ab
来自segmentfault 的GEEK作者 : https://segmentfault.com/a/1190000011241512