前言
不得不說, 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物件
複製程式碼
相關連結
阮一峰 ES6 : es6.ruanyifeng.com/#docs/promi…
知乎例子 : zhuanlan.zhihu.com/p/29632791
掘金 卡姆愛卡姆 : juejin.im/post/5b2f02…
來自segmentfault 的GEEK作者 : segmentfault.com/a/119000001…
個人部落格 : blog.pengdaokuan.cn:4001