ES6+ Promise 進(jìn)階
1. 前言
前兩節(jié)我們學(xué)習(xí)了 Promise 的用法,并且在上一節(jié)我們動(dòng)手實(shí)現(xiàn)了一個(gè)符合 Promise A+ 規(guī)范的簡(jiǎn)版 Promise。真正了解了 Promise 底層是怎么來實(shí)現(xiàn)的,更好地幫助我們理解 Promise 并對(duì) Promise 的擴(kuò)張打下了基礎(chǔ)。對(duì) Promise 的擴(kuò)展會(huì)可以解決一些通用的問題,比如使用 Promise.all()
去并發(fā)請(qǐng)求接口。在 node 中還提供了將 callback
類型的 api 轉(zhuǎn)換為 Promise 對(duì)象。
本節(jié)我們將繼續(xù)學(xué)習(xí) Promise 對(duì)象相關(guān)API的使用。這些api在我們的實(shí)際應(yīng)用中會(huì)經(jīng)常使用,并且可以很好的解決常見的問題。
2. Promise.resolve () 和 Promise.reject ()
前面我們已經(jīng)學(xué)習(xí)了在 new Promise()
對(duì)象時(shí)執(zhí)行器會(huì)提供兩個(gè)回調(diào)函數(shù),一個(gè)是 resolve
返回一個(gè)立即成功的 Promise,一個(gè)是 reject
返回一個(gè)立即失敗的 Promise。在執(zhí)行器中需要根據(jù)不同情況調(diào) resolve
或 reject
,如果我們只想返回一個(gè)成功或失敗的 Promise 怎么做呢?
Promise 對(duì)象上的提供了 Promise.resolve(value)
和 Promise.reject(reason)
語(yǔ)法糖,用于只返回一個(gè)成功或失敗的 Promise。下面我們看下它的對(duì)比寫法:
const p1 = new Promise(function(resolve, reject){
reslove(100)
})
const p2 = Promise.resolve(100) //和p1的寫法一樣
const p3 = new Promise(function(resolve, reject){
reject('error')
})
const p4 = Promise.reject('error') //和p3的寫法一樣
通過上面的對(duì)比 Promise.resolve(value)
創(chuàng)建的實(shí)例也具有 then 方法的鏈?zhǔn)秸{(diào)用。這里有個(gè)概念就是:如果一個(gè)函數(shù)或?qū)ο?,具?then 方法,那么他就是 thenable 對(duì)象。
Promise.resolve(123).then((value) => {
console.log(value);
});
Promise.reject(new Error('error')).then(() => {
// 這里不會(huì)走 then 的成功回調(diào)
}, (err) => {
console.error(err);
});
其實(shí),實(shí)現(xiàn) Promise.resolve(value)
和 Promise.reject(reason)
的源碼是很簡(jiǎn)單的。就是在 Promise 類上創(chuàng)建 resolve
和 reject
這個(gè)兩個(gè)方法,然后去實(shí)例化一個(gè) Promise 對(duì)象,最后分別在執(zhí)行器中的 resolve()
和 reject()
函數(shù)。按照這個(gè)思路有如下實(shí)現(xiàn)方式:
class Promise {
...
resolve(value) {
return new Promise((resolve, reject) => {
resolve(value)
})
}
reject(reason) {
return new Promise((resolve, reject) => {
reject(reason)
})
}
}
通過上面的實(shí)現(xiàn)源碼我們很容易地知道,這兩個(gè)方法的用法。需要注意的是 Promise.resolve(value)
中的 value 是一個(gè) Promise 對(duì)象 或者一個(gè) thenable 對(duì)象,Promise.reject(reason)
傳入的是一個(gè)異常的原因。
3. catch()
Promise 對(duì)象提供了鏈?zhǔn)秸{(diào)用的 catch 方法捕獲上一層錯(cuò)誤,并返回一個(gè) Promise 對(duì)象。catch 其實(shí)就是 then 的一個(gè)別名,目的是為了更好地捕獲錯(cuò)誤。它的行為和 Promise.prototype.then(undefined, onRejected)
只接收 onRejected
回調(diào)是相同的,then 第二個(gè)參數(shù)是捕獲失敗的回調(diào)。所以我們可以實(shí)現(xiàn)一個(gè) catch 的源碼,如下:
class Promise {
//...
catch(errorCallback) {
return this.then(null, errorCallback);
}
}
從上面的實(shí)現(xiàn) catch 的方法我們可以知道,catch 是內(nèi)部調(diào)用了 then 方法并把傳入的回調(diào)傳入到 then 的第二個(gè)參數(shù)中,并返回這個(gè) Promise。這樣我們就更清楚地知道 catch 的內(nèi)部原理了,以后看到 catch 可以直接把它看成調(diào)用了 then 的失敗的回調(diào)就行。下面我們看幾個(gè)使用 catch 的例子:
let promise = new Promise((resolve, reject) => {
resolve('100');
})
promise.then((data) => {
console.log('data:', data); // data: 100
throw new Error('error')
}, null).catch(reason => {
console.log(reason) // Error: error
})
catch 后還可以鏈?zhǔn)秸{(diào)用then方法,默認(rèn)會(huì)返回 undefined。也可以返回一個(gè)普通的值或者是一個(gè)新的 Promise 實(shí)例。同樣,在 catch 中如果返回的是一個(gè)普通值或者是 resolve,在下一層還是會(huì)被 then 的成功回調(diào)所捕獲。如果在 catch 中拋出異常或是執(zhí)行 reject 則會(huì)被下一層 then 的失敗的回調(diào)所捕獲。
promise.then((data) => {
console.log('data:', data); // data: 100
throw new Error('error')
}, null).catch(reason => {
console.log(reason) // Error: error
return 200
}).then((value) => {
console.log(value) // 200
}, null)
4. finally()
finally 是 ES9 的規(guī)范,它也是 then 的一個(gè)別名,只是這個(gè)方法是一定會(huì)執(zhí)行的,不像上面提到的 catch 只有在上一層拋出異?;蚴菆?zhí)行 reject 時(shí)才會(huì)走到 catch 中。
Promise.resolve('123').finally(() => {
console.log('100') // 100
})
知道 finally 是 then 的一個(gè)別名,那我們就知道在它后面也是可以鏈?zhǔn)秸{(diào)用的。
Promise.resolve('123').finally(() => {
console.log('100')
return 200
}).then((data) => {
console.log(data) // 123
})
需要注意的是在 finally 中返回的普通值或是返回一個(gè) Promise 對(duì)象,是不會(huì)傳到下一個(gè)鏈?zhǔn)秸{(diào)用的 then 中的。如果 finally 中返回的是一個(gè)異步的 Promise 對(duì)象,那么鏈?zhǔn)秸{(diào)用的下一層 then 是要等待 finally 有返回結(jié)果后才會(huì)執(zhí)行:
Promise.resolve('123').finally(() => {
console.log('100')
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(100)
}, 3000)
})
}).then((data) => {
console.log(data) // 123
})
執(zhí)行上面的代碼,在 then 中打印的結(jié)果會(huì)在 3 秒后執(zhí)行。這也說明了 finally 有類似 sleep 函數(shù)的意思。
finally 是 ES9 的規(guī)范,在不兼容 ES9 的瀏覽器中就不能使用這個(gè) api,所以我們可以在 Promise 對(duì)象的原型上增加一個(gè) finally 方法。
Promise.prototype.finally = function(callback) {
return this.then((value) => {
return Promise.resolve(callback()).then(() => value);
}, (err) => {
return Promise.reject(callback()).catch(() => {throw err});
})
}
因?yàn)?finally 是一定會(huì)執(zhí)行的,所以 then 中的成功和失敗的回調(diào)都需要執(zhí)行 finally 的回調(diào)函數(shù)。使用 Promise.resolve(value)
和 Promise.reject(reason)
去執(zhí)行 finally 傳入的回調(diào)函數(shù),然后使用 then 和 catch 來返回 finally 上一層返回的結(jié)果。
3. Promise.all () 和 Promise.race ()
在前端面試中經(jīng)常會(huì)問這兩個(gè) api 并做對(duì)比,因?yàn)樗鼈兊膮?shù)都是傳入一個(gè)數(shù)組,都是做并發(fā)請(qǐng)求使用的。
3.1 Promise.all()
Promise.all()
特點(diǎn)是將多個(gè) Promise 實(shí)例包裝成一個(gè)新的 Promise 實(shí)例,只有同時(shí)成功才會(huì)返回成功的結(jié)果,如果有一個(gè)失敗了就會(huì)返回失敗,在使用 then 中拿到的也是一個(gè)數(shù)組,數(shù)組的順序和傳入的順序是一致的。
const p1 = Promse.resolve('任務(wù)1');
const p2 = Promse.resolve('任務(wù)2');
const p3 = Promse.reject('任務(wù)失敗');
Promise.all([p1, p2]).then((res) => {
console.log(res); // ['任務(wù)1', '任務(wù)2']
}).catch((error) => {
console.log(error)
})
Promise.all([p1, p3, p2]).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error) // 任務(wù)失敗
})
Promise.all()
在處理多個(gè)任務(wù)時(shí)是非常有用的,比如 Promise 基礎(chǔ) 一節(jié)中使用 Promise.all()
并發(fā)的請(qǐng)求接口的案例,我們希望得到所以接口請(qǐng)求回來的數(shù)據(jù)之后再去做一些邏輯,這樣我們就不需要維護(hù)一個(gè)數(shù)據(jù)來記錄接口請(qǐng)求有沒有完成,而且這樣請(qǐng)求的好處是最大限度地利用瀏覽器的并發(fā)請(qǐng)求,節(jié)約時(shí)間。
3.2 Promise.race()
Promise.race()
和 Promise.all()
一樣也是包裝多個(gè) Promise 實(shí)例,返回一個(gè)新的 Promise 實(shí)例,只是返回的結(jié)果不同。Promise.all()
是所有的任務(wù)都處理完才會(huì)得到結(jié)果,而 Promise.race()
是只要任務(wù)成功就返回結(jié)果,無(wú)論結(jié)果是成功還是失敗。
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('任務(wù)1成功...');
}, 1000)
})
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('任務(wù)2成功...');
}, 1500)
})
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('任務(wù)失敗...');
}, 500)
})
Promise.race([p1, p2]).then((res) => {
console.log(res); // 任務(wù)1成功...
}).catch((err) => {
console.log(err);
})
Promise.race([p1, p2, p3]).then((res) => {
console.log(res)
}).catch((err) => {
console.log(err) // 任務(wù)失敗...
})
上面的實(shí)例代碼充分的展示了 Promise.race()
特性,在實(shí)際的開發(fā)中很少用到這個(gè) api,這個(gè) api 能做什么用呢?其實(shí)這個(gè) api 可以用在一些請(qǐng)求超時(shí)時(shí)的處理。
當(dāng)我們?yōu)g覽網(wǎng)頁(yè)時(shí),突然網(wǎng)絡(luò)斷開或是變得很差的情況下,可以用于提示用戶網(wǎng)絡(luò)不佳,這也是一個(gè)比較常見的情況。這個(gè)時(shí)候我們就可以使用 Promise.race()
來處理:
const request = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('請(qǐng)求成功...');
}, 3000);
})
const timeout = new Promise((resolve, reject) => {
setTimeout(() => {
reject('請(qǐng)求超時(shí),請(qǐng)檢查網(wǎng)絡(luò)...');
}, 2000);
})
Promise.race([request, timeout]).then(res => {
console.log(res);
}, err => {
console.log(err); // 請(qǐng)求超時(shí),請(qǐng)檢查網(wǎng)絡(luò)...
})
上面的代碼中定義了兩個(gè) Promise 實(shí)例,一個(gè)是請(qǐng)求實(shí)例,一個(gè)是超時(shí)實(shí)例。請(qǐng)求實(shí)例當(dāng) 3 秒的時(shí)候才會(huì)返回,而超時(shí)設(shè)置了 2 秒,所以會(huì)先返回超時(shí)的結(jié)果,這樣就可以去提醒用戶了。
3.3 實(shí)現(xiàn) Promise.all ()
面試題:實(shí)現(xiàn)一個(gè)
Promise.all()
方法。
前面我們說到了 thenable 對(duì)象,也就是判斷一個(gè)值是不是 Promise 對(duì)象,就是判斷它是函數(shù)或?qū)ο?,并具?then 方法。
const isPromise = (val) => {
if (typeof val === "function" || (typeof val == "object" && val !== null)) {
if (typeof val.then === "function") {
return true;
}
}
return false;
};
Promise.all()
會(huì)接收一個(gè)數(shù)組,數(shù)組的每一項(xiàng)都是一個(gè) Promise 實(shí)例,并且它的返回結(jié)果也是一個(gè) Promise,所以我們需要在內(nèi)部 new 一個(gè) Promise 對(duì)象,并返回。在執(zhí)行器中我們的目標(biāo)是:
- 當(dāng)有實(shí)例中有錯(cuò)誤或拋出異常時(shí),就要執(zhí)行執(zhí)行器中的 reject;
- 沒有錯(cuò)誤時(shí),只有所有的實(shí)例都成功時(shí)才會(huì)執(zhí)行執(zhí)行器中的 resolve。
基于這兩點(diǎn),有如下步驟:
- 內(nèi)部創(chuàng)建一個(gè)計(jì)數(shù)器,用于記住已經(jīng)處理的實(shí)例,當(dāng)計(jì)數(shù)的值和傳入實(shí)例的數(shù)組長(zhǎng)度相等時(shí),執(zhí)行執(zhí)行器中的 resolve;
- 創(chuàng)建一個(gè)用于存放實(shí)例返回結(jié)果的數(shù)組;
- 處理實(shí)例的結(jié)果有兩種:一種返回的是普通值、一種返回的是 Promise 對(duì)象,然后分別處理;
- 返回普通值結(jié)果時(shí)直接存放到數(shù)組中即可;
- 返回的是一個(gè) Promise 對(duì)象時(shí),就需要調(diào)用這個(gè)實(shí)例上的 then 方法得到結(jié)果后在存放到結(jié)果數(shù)組中去。
根據(jù)上面的五個(gè)步驟基本就可以把 Promise.all()
實(shí)現(xiàn)出來了,具體代碼如下:
Promise.all = function(arr) {
return new Promise((resolve, reject) => {
let num = 0; // 用于計(jì)數(shù)
const newArr = []; // 存放最終的結(jié)果
function processValue(index, value) { // 處理Promise實(shí)例傳入的結(jié)果
newArr[index] = value;
if (++num == arr.length) { // 當(dāng)計(jì)數(shù)器的值和處理的 Promise 實(shí)例的長(zhǎng)度相當(dāng)時(shí)統(tǒng)一返回保護(hù)所以結(jié)果的數(shù)組
resolve(newArr);
}
}
for (let i = 0; i < arr.length; i++) {
const currentValue = arr[i]; // Promise 實(shí)例
if (isPromise(currentValue)) {
currentValue.then((res) => {
processValue(i, res);
}, reject)
} else {
processValue(i, currentValue);
}
}
});
}
上面的代碼已經(jīng)實(shí)現(xiàn)了 Promise.all()
方法,可以使用下面的例子進(jìn)行測(cè)試。
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("任務(wù)1成功...");
}, 1000);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("任務(wù)2成功...");
}, 500);
});
Promise.all([p1, p2]).then((res) => {
console.log(res)
})
4. 小結(jié)
本節(jié)學(xué)習(xí)了根據(jù) Promise 衍生出的相關(guān) api 的使用,已經(jīng)每個(gè) api 基本都給出了實(shí)現(xiàn)源碼,理解這些源碼會(huì)讓我們更加深刻地理解 Promise,在實(shí)際的開發(fā)過程中達(dá)到游刃有余。到此我們花了三節(jié)的時(shí)間由淺入深來介紹 Promise,花些時(shí)間來徹底弄懂這些知識(shí)點(diǎn),對(duì)于我們以后學(xué)習(xí)其他的異步解決方案有更好的理解。