[注:以下代碼都在支持 Promise 的 Node 環(huán)境中實(shí)現(xiàn)]
1 promise 釋義
promise 是抽象異步處理的對(duì)象,其提供了一系列處理異步操作的方法。
1.1 語法
const promiseA = new Promise((resolve, reject)=>{
// 異步操作
// 操作結(jié)束,使用 resolve()返回結(jié)果;使用 reject()處理錯(cuò)誤
})
promiseA.then(onFulfilled, onRejected);
例子1-1:
const promiseA = new Promise(()=>{
setTimeout(()=>{
resolve('3秒后返回了A');
}, 3000)
});
promiseA.then((res)=>{
console.log(res);
});
1.2 static method
像 Promise 這樣的全局對(duì)象還擁有一些靜態(tài)方法。
包括 Promise.all() 還有 Promise.resolve() 等在內(nèi),主要都是一些對(duì)Promise進(jìn)行操作的輔助方法。
1.2.1 Promise.all
Promise.all 接收一個(gè)promise對(duì)象數(shù)組作為參數(shù),當(dāng)這個(gè)數(shù)組里的所有promise對(duì)象全部變?yōu)?code>resolve或reject狀態(tài)的時(shí)候,它才會(huì)去調(diào)用.then方法。
用于需要同時(shí)觸發(fā)多個(gè)異步操作,并在所有異步操作都執(zhí)行結(jié)束以后才調(diào)用.then。
Promise.all 里有一個(gè) promise 返回錯(cuò)誤的時(shí)候就調(diào)用 catch() 了。測(cè)試代碼如下:
const promiseA = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('promise A.');
}, 1000);
});
const promiseB = new Promise((resolve, reject) => {
setTimeout(() => {
reject('error B');
// resolve('promise B');
}, 1500);
});
const promiseC = new Promise((resolve, reject) => {
setTimeout(() => {
reject('error c');
}, 1000);
});
Promise.all([promiseA, promiseB, promiseC]).then((res)=>{
console.log(res);
}).catch((err) => {
console.log(err);
});
// 結(jié)果:error c
這點(diǎn)和預(yù)期的不同。具體描述可以看 MDN 的文檔,這里摘錄一部分:
The
Promise.all()method returns a singlePromisethat resolves when all of the promises in the iterable argument have resolved or when the iterable argument contains no promises. It rejects with the reason of the first promise that rejects.
- 思考:那么在并行執(zhí)行所有
promise過程中,在存在reject的情況下如何獲取其余resolve的全部結(jié)果?
似乎并沒有單獨(dú)的method來處理,需要封裝一個(gè)方法。
1.2.3 Promise.race
Promise.race和Promise.all類似,同樣對(duì)多個(gè)promise對(duì)象進(jìn)行處理,同樣接收一個(gè)promise對(duì)象數(shù)組。Promise.race只要有一個(gè)promise對(duì)象進(jìn)入Fullfilled或者Rejected狀態(tài)的話,就會(huì)執(zhí)行.then或.catch方法。
測(cè)試代碼如下:
const promiseA = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('promise A.');
}, 1000);
});
const promiseB = new Promise((resolve, reject) => {
setTimeout(() => {
// reject('error B');
resolve('promise B');
}, 1500);
});
const promiseC = new Promise((resolve, reject) => {
setTimeout(() => {
// reject('error c');
resolve('promise c');
}, 500);
});
Promise.race([promiseA, promiseB, promiseC]).then((res)=>{
console.log(res);
}).catch((err) => {
console.log(err);
});
1.2.2 Promise.resolve
靜態(tài)方法Promise.resolve(value)可以認(rèn)為是new Promise()方法的快捷方式。如:
Promise.resolve(42).then((value)=>{
console.log(value);
})
但初始化Promise對(duì)象建議仍然使用new Promise,Promise.resove的另一個(gè)作用是將thenable對(duì)象轉(zhuǎn)換為promise對(duì)象。
ES6 Promise里提到了Thenable的概念,簡(jiǎn)單來講它是非常類似于promise的東西。就好像有些具有.length方法的非數(shù)組對(duì)象被稱為Array like,thenable指的是具有.then方法的對(duì)象。
這種將thenable對(duì)象轉(zhuǎn)換為promise對(duì)象的機(jī)制要求thenable對(duì)象所擁有的then方法應(yīng)該和Promise所擁有的then 方法具有同樣的功能和處理過程,在將thenable對(duì)象轉(zhuǎn)換為promise``對(duì)象的時(shí)候,還會(huì)巧妙的利用thenable對(duì)象原來具有的then方法。最簡(jiǎn)單的例子就是jQuery.ajax(),它的返回值就是thenable。下面看看如何將thenable對(duì)象轉(zhuǎn)換為promise對(duì)象。
const promiseA = Promise.resolve($.ajax('/json/comment.json')); // => promise 對(duì)象
promiseA.then((value)=>{
console.log(value);
})
需要注意的是jQuery.ajax()返回的是一個(gè)具有.then方法的jqXHR Object對(duì)象,這個(gè)對(duì)象繼承了來自Deferred Object的方法和屬性。
但是Deferred Object并沒有遵循PormisesA+或ES6 Promises標(biāo)準(zhǔn),所以即使看上去對(duì)象轉(zhuǎn)換為了promise對(duì)象,其實(shí)還是缺失了部份信息。即使一個(gè)對(duì)象具有.then方法,也不一定就能作為ES6 Promises對(duì)象使用。
這種轉(zhuǎn)換 thenable的功能除了在編寫使用Promises的類庫的時(shí)候需要了解之外,通常作為end-user不會(huì)使用到此功能。
1.2.3 Promise.reject
通過調(diào)用Promise.reject()可以將錯(cuò)誤對(duì)象傳遞給onRejected 函數(shù)。
Promise.reject(new Error("BOOM!"))
.catch((error)){
console.log(error);
}
這個(gè)方法并不常用。
1.3 promise 狀態(tài)
用 new Promise實(shí)例化的 promise 對(duì)象有三種狀態(tài):
- 'has resolution' => 'Fulfilled'
resolve(成功)時(shí),會(huì)調(diào)用 onFulfilled。 - 'has rejected' => 'Rejected'
reject(失敗)時(shí),會(huì)調(diào)用 onRejected。 - 'unresolved' => 'Pending'
promise 對(duì)象剛被創(chuàng)建后的初始狀態(tài)。
promise對(duì)象的狀態(tài),從 Pending 轉(zhuǎn)換為 Fulfilled 或 Rejected 之后,promise 對(duì)象的狀態(tài)就不再改變。因此,在 .then()內(nèi)執(zhí)行的函數(shù)只會(huì)調(diào)用一次。
異常處理:then or catch?
.catch 方法可以理解為 promise.then(undefined, onRejected)。但兩者有不同之處:
- 使用promise.then(onFulfilled, onRejected) 的話,在 onFulfilled 中發(fā)生異常的話,在 onRejected 中是捕獲不到這個(gè)異常的。
- 在 promise.then(onFulfilled).catch(onRejected) 的情況下,then 中產(chǎn)生的異常能在 .catch 中捕獲。
- then 和 .catch 在本質(zhì)上是沒有區(qū)別的,但需要根據(jù)1,2點(diǎn)的差異選擇適用的場(chǎng)合。
測(cè)試對(duì)比代碼如下:
const promiseA = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('1s test.');
}, 1000);
});
promiseA.then((res)=>{
throw new Error('handler err');
}).catch((err)=>{
console.log(`promiseA ${err}`);
})
const promiseA = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('1s test.');
}, 1000);
});
promiseA.then((res) => {
throw new Error('handler err');
}, (err)=>{
console.log(`promiseA ${err}`);
});
2 async/await 簡(jiǎn)介
Node7 通過 --harmony_async_await參數(shù)支持 async/await ,而 async/await 由于其可以用同步形式的代碼書寫異步操作,能徹底杜絕‘回調(diào)地獄’式代碼。
async/await 基于 Promise, 是 Generator 函數(shù)的語法糖。async 函數(shù)返回一個(gè) Promise 對(duì)象,可以使用 then 方法添加回調(diào)函數(shù)。當(dāng)函數(shù)執(zhí)行時(shí),一旦遇到await就先返回,等到觸發(fā)的異步操作完成,再接著執(zhí)行函數(shù)體后面的語句。示例代碼如下:
function asynchornous(timer) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('測(cè)試 async/await');
}, timer);
});
}
async function test() {
const time0 = new Date();
const res = await asynchornous(2000);
const time1 = new Date();
console.log(`返回 => ${res},用時(shí):${Math.floor((time1 - time0)/1000)}s`);
}
test();
2.1 await 的用法
await 命令必須用到 async 函數(shù)中,且其后應(yīng)該是一個(gè) Promise 對(duì)象。如果不是,會(huì)被轉(zhuǎn)化為一個(gè)立即 resolve 的 Promise 對(duì)象。
只要一個(gè) await命令后面的 Promise 對(duì)象變?yōu)?reject 狀態(tài),那么整個(gè) async 函數(shù)都會(huì)中斷執(zhí)行。
async function test() {
await Promise.reject('error');
await Promise.resolve('test'); // 不會(huì)執(zhí)行
}
這時(shí)如果我們希望前一個(gè)異步操作失敗后,不中斷后面的異步操作,可以捕獲前一個(gè)異步操作的錯(cuò)誤。另一種寫法是在 await后面的 Promise 對(duì)象后再跟上 catch方法。示例代碼如下:
async function test() {
await Promise.reject('error')
.catch(err => console.log(err));
const res = await Promise.resolve(`test`);
console.log(res);
}
test();
// 執(zhí)行結(jié)果:
// error
// test
2.2 捕獲錯(cuò)誤
await 命令后的 Promise對(duì)象,運(yùn)行結(jié)果可能是 rejected,這樣等同于 async函數(shù)返回的 Promise 狀態(tài)為 rejected。 所以可以把 await 命令放到 try...catch 代碼中。示例代碼如下:
function asynchornous(timer) {
return new Promise((resolve, reject) => {
setTimeout(() => {
// resolve('測(cè)試 async/await');
reject('error test');
}, timer);
});
}
async function test() {
const time0 = new Date();
let res = '...';
try {
res = await asynchornous(2000);
} catch (error) {
console.log(`返回 => ${error}`);
}
const time1 = new Date();
console.log(`返回 => ${res},用時(shí):${Math.floor((time1 - time0)/1000)}s`);
}
test();
// 執(zhí)行后返回結(jié)果如下:
// 返回 => error test
// 返回 => ...,用時(shí):2s
2.3 并發(fā)執(zhí)行
如果 多個(gè) await 后面的異步操作,不存在依賴關(guān)系,那么最好讓它們都并發(fā)執(zhí)行。使用 Promise.all 可以讓多個(gè) promise 并發(fā),同時(shí)還有另一種寫法。
示例代碼如下:
// 寫法一
let [resA, resB] = await Promise.all([testA(), testB]);
// 寫法二
let proA = testA();
let proB = testB();
let resA = await proA;
let resB = await proB;
上述寫法,testA 和 testB 都是同時(shí)觸發(fā)的。那么再看看繼發(fā)執(zhí)行的代碼:
let resA = await proA();
let resB = await proB();
3 改寫 callback 方式
Node 很多庫函數(shù),還有很多第三方庫函數(shù)都是使用回調(diào)實(shí)現(xiàn),那么要如何修改為 Promise 實(shí)現(xiàn)?
- 使用第三方庫,如:Async,Q,Bluebird 等,具體實(shí)現(xiàn)請(qǐng)參考官方文檔和附錄參考3。
- 自己實(shí)現(xiàn)一個(gè)將回調(diào)風(fēng)格轉(zhuǎn)變?yōu)?Promise 風(fēng)格的類庫。
這里詳細(xì)講解如何實(shí)現(xiàn)回調(diào)函數(shù)的轉(zhuǎn)換函數(shù)。
3.1 定義 promisify()
promisify 是一個(gè)轉(zhuǎn)換函數(shù),它的參數(shù)是需要轉(zhuǎn)換的回調(diào)函數(shù),那么返回值則是一個(gè)返回 promise對(duì)象的函數(shù)。如下:
function promisify(callback) {
return function(){
return new Promise((resolve, reject)=>{
// TODO:
})
}
}
3.2 Promise 中調(diào)用 callback
要讓回調(diào)函數(shù)在 Promise 中調(diào)用,并且根據(jù)結(jié)果適當(dāng)?shù)恼{(diào)用resolve()和reject()。
function promisify(callback) {
return function(){
return new Promise((resolve, reject)=>{
callbacn((error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
})
})
}
}
注意,Node 回調(diào)函數(shù)第一個(gè)參數(shù)都是錯(cuò)誤對(duì)象,如果為 null 表示沒有錯(cuò)誤。
3.3 添加參數(shù)
繼續(xù)添加處理參數(shù)的代碼。Node 回調(diào)函數(shù)通常前面 n 個(gè)參數(shù)是內(nèi)部實(shí)現(xiàn)需要使用的參數(shù),而最后一個(gè)參數(shù)是回調(diào)函數(shù)。因此可以使用 ES6 的可變參數(shù)和擴(kuò)展數(shù)據(jù)語法來實(shí)現(xiàn)。代碼如下:
function promisify(callback) {
return function(...args){
return new Promise((resolve, reject)=>{
callback(...args, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
})
})
}
}
3.4 實(shí)現(xiàn) promisifyObject()
顧名思義,promisifyObject() 是用來轉(zhuǎn)換對(duì)象中異步方法的回調(diào)函數(shù)。轉(zhuǎn)換函數(shù)必須考慮this 指針的問題,所以不能直接使用上面的一般實(shí)現(xiàn)。下面是 promisify() 的簡(jiǎn)化實(shí)現(xiàn),詳情請(qǐng)參考代碼中的注釋。
function promisifyObject(obj, suffx = 'Promisified') {
// 參照之前的實(shí)現(xiàn),重新實(shí)現(xiàn) promisify.
// 這個(gè)函數(shù)沒用到外層的局部變量,不必實(shí)現(xiàn)局域函數(shù)
// 這里實(shí)現(xiàn)為局部函數(shù)只是為了組織演示代碼
function promisify(callback){
return function(...args) {
return new Promise((resolve, reject) => {
// 注意調(diào)用的方式有了改變
callback.call(this, ...args, (error, result) => {
if (error) {
reject(error)
} else {
resolve(result)
}
})
})
}
}
// 先找出所有方法名稱
// 如果需要過濾可以添加 filter 實(shí)現(xiàn)
const keys = [];
for (const key in obj) {
if(typeof obj[key] === 'function') {
keys.push(key);
}
}
// 將轉(zhuǎn)換之后的函數(shù)仍然附加到原對(duì)象上,
// 以確保調(diào)用時(shí)候,this 引用正確。。
// 為了避免覆蓋原函數(shù),`promise`風(fēng)格的函數(shù)名前添加‘suffix’.
keys.forEach(key => {
obj[`${key}${suffix}`] = promisify(obj[key]);
})
return obj;
}
3.5 將轉(zhuǎn)換 Promise 的函數(shù)封裝成模塊
實(shí)現(xiàn)很簡(jiǎn)單,具體代碼如下:
module.exports = {
promisify,
promisifyObjecj
}
// 通過解構(gòu)對(duì)象導(dǎo)入
// const {promisify, promisifyObject} = require('./promisify');
3.6 實(shí)際場(chǎng)景應(yīng)用
這里使用實(shí)際項(xiàng)目中用到的 qiniu api 存圖場(chǎng)景中異步回調(diào)被改寫后如何使用 async/await,示例代碼如下:
function saveImage(...args) {
// bucketManager 是 qiniu api 里操作存儲(chǔ)空間的對(duì)象,
// .fetch 方法是用來上傳內(nèi)容的方法
return new Promise((resolve, reject) => {
bucketManager.fetch(resUrl, bucket, key, (err, res) => {
if (err) {
reject(err);
} else {
resolve(res);
}
});
});
}
async function expand() {
try {
const response = await saveImage('', 'hexo', 'qiuniu_api_test.jpg');
console.log('res', response);
} catch (error) {
console.log('err', error);
}
}
expand();
4 jest 測(cè)試
最后我們嘗試使用 jest 來測(cè)試以 Promise 為基礎(chǔ)的異步代碼。
示例1:
function sleep(timer, state) {
return new Promise(((reslove) => {
setTimeout(() => {
// things
reslove('sleep:ok');
if (state === 404) {
throw new Error('sleep:這里有個(gè) 404');
}
}, timer);
}));
}
// The assertion for a promise must be returned.
it('works with promises', () => {
expect.assertions(1); // ?
return sleep(1000, 200).then(result => expect(result).toEqual('sleep:ok'));
});
示例1 測(cè)試的返回 promise示例的函數(shù),需要設(shè)置 expect.assertions(1),然后將期望函數(shù)寫到 .then 方法中即可。
示例2:
function asynchornous(timer) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('測(cè)試 async/await');
}, timer);
});
}
// async/await can be used.
it('works with async/await', async () => {
expect.assertions(1);
const data = await asynchornous(1000);
expect(data).toEqual('測(cè)試 async/await');
});
代碼同樣很簡(jiǎn)單,更多的示例可以查看 jest 的官網(wǎng)文檔。