前言
不得不說堪藐, promise 這玩意届榄,是每個面試官都會問的問題浅乔,但是你真的了解promise嗎?其實(shí)我也不了解痒蓬,下面的內(nèi)容都是我從掘金童擎、知乎、《ECMAScript6入門》上看的博客文章等資料攻晒,然后總結(jié)的顾复,畢竟自己寫一遍,更有助于理解鲁捏,如有錯誤芯砸,請指出 ~
什么是回調(diào)地獄 ?
在過去寫異步代碼都要靠回調(diào)函數(shù)给梅,當(dāng)異步操作依賴于其他異步操作的返回值時假丧,會出現(xiàn)一種現(xiàn)象,被程序員稱為 “回調(diào)地獄”动羽,比如這樣 :
// 假設(shè)我們要請求用戶數(shù)據(jù)信息包帚,它接收兩個回調(diào),假設(shè)我們要請求用戶數(shù)據(jù)信息运吓,它接收兩個回調(diào)渴邦,successCallback 和 errCallback
function getUserInfo (successCallback, errCallback) {
$.ajax({
url : 'xxx',
method : 'get',
data : {
user_id : '123'
},
success : function(res) {
successCallback(res) // 請求成功,執(zhí)行successCallback()回調(diào)
},
error : function(err) {
errCallback(err) // 請求失敗拘哨,執(zhí)行errCallback()回調(diào)
}
})
}
騙我 谋梭? 這哪里復(fù)雜了,明明很簡單啊倦青,說好的回調(diào)地獄呢 瓮床? 不急,繼續(xù)看
假設(shè)我們拿到了用戶信息,但是我們還要拿到該用戶的聊天列表隘庄,然后再拿到跟某一“陌生”男人的聊天記錄呢 ?
// 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('獲取用戶個人信息失敗')
})
大兄弟贾虽,刺激不逃糟,三層嵌套吼鱼,再多來幾個嵌套,就是 “回調(diào)地獄” 了绰咽。這時候菇肃,promise來了。
Promise 簡介
阮一峰老師的《ECMAScript 6入門》里對promise的含義是 : Promise 是異步編程的一種解決方案取募,簡單說就是一個容器琐谤,里面保存著某個未來才會結(jié)束的事件(通常是一個異步操作)的結(jié)果。從語法上說玩敏,Promise 是一個對象斗忌,從它可以獲取異步操作的消息。Promise 提供統(tǒng)一的 API旺聚,各種異步操作都可以用同樣的方法進(jìn)行處理织阳。
簡單來說,Promise就是對異步的執(zhí)行結(jié)果的描述對象砰粹。
狀態(tài)
- pending (進(jìn)行中)
- fulfilled (已成功)
- rejected (已失敗)
1 : 只有異步操作的結(jié)果唧躲,可以決定當(dāng)前是哪一種狀態(tài),任何其他操作都無法改變這個狀態(tài)碱璃。
2 : 一旦狀態(tài)改變弄痹,就不會再變,任何時候都可以得到這個結(jié)果嵌器。
3 : Promise對象的狀態(tài)改變肛真,只有兩種可能:從pending變?yōu)閒ulfilled和從pending變?yōu)閞ejected
知乎形象例子來說明promise
// 定外賣就是一個Promise,Promise的意思就是承諾
// 我們定完外賣,飯不會立即到我們手中
// 這時候我們和商家就要達(dá)成一個承諾
// 在未來爽航,不管飯是做好了還是燒糊了蚓让,都會給我們一個答復(fù)
function 定外賣(){
// Promise 接受兩個參數(shù)
// resolve: 異步事件成功時調(diào)用(菜燒好了)
// reject: 異步事件失敗時調(diào)用(菜燒糊了)
return new Promise((resolve, reject) => {
let result = 做飯()
// 下面商家給出承諾,不管燒沒燒好岳掐,都會告訴你
if (result == '菜燒好了')
// 商家給出了反饋
resolve('我們的外賣正在給您派送了')
else
reject('不好意思凭疮,我們菜燒糊了,您再等一會')
})
}
// 商家廚房做飯串述,模擬概率事件
function 做飯() {
return Math.random() > 0.5 ? '菜燒好了' : '菜燒糊了'
}
// 你在家上餓了么定外賣
// 有一半的概率會把你的飯燒糊了
// 好在有承諾执解,他還是會告訴你
定外賣()
// 菜燒好執(zhí)行,返回'我們的外賣正在給您派送了'
.then(res => console.log(res))
// 菜燒糊了執(zhí)行,返回'不好意思衰腌,我們菜燒糊了新蟆,您再等一會'
.catch(res => console.log(res))
基本用法
Promise 對象是一個構(gòu)造函數(shù),用來生成一個Promise實(shí)例右蕊。
Promise構(gòu)造函數(shù)接受一個函數(shù)作為參數(shù)琼稻,這個函數(shù)有兩個參數(shù),分別是resolve()和reject()饶囚。
resovle()函數(shù)是將Promise對象從pending變成fulfilled帕翻,在異步操作完成時執(zhí)行,將異步結(jié)果萝风,作為參數(shù)傳遞出去嘀掸。
reject()函數(shù)是將Promise對象從pending變成rejected,在異步執(zhí)行失敗時執(zhí)行规惰,將報錯信息睬塌,作為參數(shù)傳遞出去。
// 簡單的一個promise實(shí)例歇万, 來自阮一峰老師的es6 示例代碼
const promise = new Promise((resolve, reject) => {
// some code
if(/* 異步執(zhí)行成功 */) {
resolve(res)
} else {
reject(error)
}
})
then方法
Promise 有個.then()方法揩晴,then 方法中的回調(diào)在微任務(wù)隊(duì)列中執(zhí)行,支持傳入兩個參數(shù)贪磺,一個是成功的回調(diào)硫兰,一個是失敗的回調(diào),在 Promise 中調(diào)用了 resolve 方法缘挽,就會在 then 中執(zhí)行成功的回調(diào)瞄崇,調(diào)用了 reject 方法,就會在 then 中執(zhí)行失敗的回調(diào)壕曼,成功的回調(diào)和失敗的回調(diào)只能執(zhí)行一個苏研,resolve 和 reject 方法調(diào)用時傳入的參數(shù)會傳遞給 then 方法中對應(yīng)的回調(diào)函數(shù)。
// 執(zhí)行 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
// 執(zhí)行 reject
let promise = new Promise((resolve, reject) => {
console.log(1)
reject()
})
promise.then(()=>{
console.log(2)
}, ()=>{
console.log(3)
})
// 1
// 3
then方法
[ 注意 : then方法中的回調(diào)是異步的H肌D∧ⅰ!]
為什么上面第一個示例代碼的結(jié)果是 1 -> 2 -> 3呢 轧飞?傳入Promise 中的執(zhí)行函數(shù)是立即執(zhí)行完的啊衅鹿,為什么不是立即執(zhí)行 then 中的回調(diào)呢?因?yàn)閠hen 中的回調(diào)是異步執(zhí)行过咬,表示該回調(diào)是插入事件隊(duì)列末尾大渤,在當(dāng)前的同步任務(wù)結(jié)束之后,下次事件循環(huán)開始時執(zhí)行隊(duì)列中的任務(wù)掸绞。
Promise 的回調(diào)函數(shù)不是正常的異步任務(wù)泵三,而是微任務(wù)(microtask)。它們的區(qū)別在于,正常任務(wù)追加到下一輪事件循環(huán)烫幕,微任務(wù)追加到本輪事件循環(huán)俺抽。這意味著,微任務(wù)的執(zhí)行時間一定早于正常任務(wù)
then方法的返回值是一個新的GPromise對象较曼,這就是為什么promise能夠進(jìn)行鏈?zhǔn)讲僮鞯脑颉?
then方法中的一個難點(diǎn)就是處理異步磷斧,通過setInterval來監(jiān)聽GPromise對象的狀態(tài)改變,一旦改變捷犹,就是執(zhí)行GPromise對應(yīng)的then方法中相應(yīng)的回調(diào)函數(shù)弛饭。這樣回調(diào)函數(shù)就能夠插入事件隊(duì)列末尾赎离,異步執(zhí)行研叫。
then有兩個參數(shù) : onFulfilled 和 onRejected
· 當(dāng)狀態(tài)state為fulfilled,則執(zhí)行onFulfilled,傳入this.value翠桦。當(dāng)狀態(tài)state為rejected,則執(zhí)行onRejected胳蛮,傳入this.reason
· onFulfilled,onRejected如果他們是函數(shù)销凑,則必須分別在fulfilled,rejected后被調(diào)用仅炊,value或reason依次作為他們的第一個參數(shù)
class Promise{
constructor(executor){...}
// then 方法 有兩個參數(shù)onFulfilled onRejected
then(onFulfilled,onRejected) {
// 狀態(tài)為fulfilled斗幼,執(zhí)行onFulfilled,傳入成功的值
if (this.state === 'fulfilled') {
onFulfilled(this.value);
};
// 狀態(tài)為rejected抚垄,執(zhí)行onRejected蜕窿,傳入失敗的原因
if (this.state === 'rejected') {
onRejected(this.reason);
};
}
}
Promise的鏈?zhǔn)秸{(diào)用
由于promise每次調(diào)用then方法就會返回一個新的promise對象,如果該then方法中執(zhí)行的回調(diào)函數(shù)有返回值呆馁,那么這個返回值就會作為下一個promise實(shí)例的then方法回調(diào)的參數(shù)桐经,如果 then 方法的返回值是一個 Promise 實(shí)例,那就返回一個新的 Promise 實(shí)例浙滤,將 then 返回的 Promise 實(shí)例執(zhí)行后的結(jié)果作為返回 Promise 實(shí)例回調(diào)的參數(shù)阴挣。
還記得剛開頭說的那個“陌生”男人例子嗎 ?這里我們用promise的鏈?zhǔn)讲僮髦貙懴?/p>
// 原來的代碼
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 實(shí)例具有then方法揖膜,也就是說誓沸,then方法是定義在原型對象Promise.prototype上的。它的作用是為 Promise 實(shí)例添加狀態(tài)改變時的回調(diào)函數(shù)壹粟。前面說過拜隧,then方法的第一個參數(shù)是resolved狀態(tài)的回調(diào)函數(shù),第二個參數(shù)(可選)是rejected狀態(tài)的回調(diào)函數(shù)。
then方法返回的是一個新的Promise實(shí)例(注意虹蓄,不是原來那個Promise實(shí)例)犀呼。因此可以采用鏈?zhǔn)綄懛ǎ磘hen方法后面再調(diào)用另一個then方法薇组。
采用鏈?zhǔn)降膖hen外臂,可以指定一組按照次序調(diào)用的回調(diào)函數(shù)。這時律胀,前一個回調(diào)函數(shù)宋光,有可能返回的還是一個Promise對象(即有異步操作),這時后一個回調(diào)函數(shù)炭菌,就會等待該P(yáng)romise對象的狀態(tài)發(fā)生變化罪佳,才會被調(diào)用
來自ES6的 Promise.prototype.catch()
Promise.prototype.catch方法是.then(null, rejection)的別名,用于指定發(fā)生錯誤時的回調(diào)函數(shù)黑低。Promise對象狀態(tài)變?yōu)閞esolved赘艳,則會調(diào)用then方法指定的回調(diào)函數(shù);如果異步操作拋出錯誤克握,狀態(tài)就會變?yōu)閞ejected蕾管,就會調(diào)用catch方法指定的回調(diào)函數(shù),處理這個錯誤菩暗。另外掰曾,then方法指定的回調(diào)函數(shù),如果運(yùn)行中拋出錯誤停团,也會被catch方法捕獲旷坦。
Promise 對象的錯誤具有“冒泡”性質(zhì),會一直向后傳遞佑稠,直到被捕獲為止秒梅。也就是說讶坯,錯誤總是會被下一個catch語句捕獲
一般來說,不要在then方法里面定義 reject 狀態(tài)的回調(diào)函數(shù)(即then的第二個參數(shù))辆琅,總是使用catch方法。
來自ES6的 Promise.all()
Promise.all方法用于將多個 Promise 實(shí)例婉烟,包裝成一個新的 Promise 實(shí)例娩井。
const p = Promise.all([p1, p2, p3])
Promise.all方法接受一個數(shù)組作為參數(shù),p1洞辣、p2咐刨、p3都是 Promise 實(shí)例扬霜,如果不是定鸟,就會先調(diào)用下面講到的Promise.resolve方法,將參數(shù)轉(zhuǎn)為 Promise 實(shí)例著瓶,再進(jìn)一步處理联予。
p的狀態(tài)由p1材原、p2、p3決定余蟹,分成兩種情況。
(1)只有p1窑睁、p2兼搏、p3的狀態(tài)都變成fulfilled卵慰,p的狀態(tài)才會變成fulfilled佛呻,此時p1病线、p2、p3的返回值組成一個數(shù)組送挑,傳遞給p的回調(diào)函數(shù)。
(2)只要p1纺裁、p2司澎、p3之中有一個被rejected,p的狀態(tài)就變成rejected挤安,此時第一個被reject的實(shí)例的返回值,會傳遞給p的回調(diào)函數(shù)嫩絮。
來自ES6 的Promise.race()
Promise.race方法同樣是將多個 Promise 實(shí)例,包裝成一個新的 Promise 實(shí)例剿干。
const p = Promise.all([p1, p2, p3])
上面代碼中,只要p1杠步、p2撰洗、p3之中有一個實(shí)例率先改變狀態(tài),p的狀態(tài)就跟著改變差导。那個率先改變的 Promise 實(shí)例的返回值,就傳遞給p的回調(diào)函數(shù)
Promise.race方法的參數(shù)與Promise.all方法一樣设褐,如果不是 Promise 實(shí)例颠蕴,就會先調(diào)用下面講到的Promise.resolve方法助析,將參數(shù)轉(zhuǎn)為 Promise 實(shí)例,再進(jìn)一步處理寡键。
來自ES6 的Promise.resolve()
有時需要將現(xiàn)有對象轉(zhuǎn)為 Promise 對象雪隧,Promise.resolve方法就起到這個作用
Promise.resolve('test')
// 等價于
new Promise(resolve => resolve('test'))
// 更多請看阮一峰老師的ES6 Promise對象
來自ES6 的Promise.reject()
Promise.reject(reason)方法也會返回一個新的 Promise 實(shí)例,該實(shí)例的狀態(tài)為rejected藕畔。
const p = Promise.reject('出錯了');
// 等同于
const p = new Promise((resolve, reject) => reject('出錯了'))
p.then(null, function (err) {
console.log(err) // 出錯了
});
// 更多請看阮一峰老師的ES6 Promise對象
相關(guān)鏈接
個人博客 : 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