關(guān)于Promise的基本內(nèi)容,已經(jīng)寫過一篇文章。基本的會用,偶爾也用過,不過知識點(diǎn)比較多,并且代碼在外面套了好幾層,感覺也比較復(fù)雜,一直理不順頭緒。最近看了下面鏈接的幾篇文章,感覺很不錯,對于Promise基本概念的理解比以前要清晰一點(diǎn)了。
Promise的知識點(diǎn)很多,這里是一次從多到少的收斂過程,寫了幾點(diǎn)平時可能會用到的最基礎(chǔ)的用法。
大白話講解Promise(一)
這篇文章寫得還是比較簡單直接的,對于理解概念很有幫助,推薦好好看看。
廖雪峰的Promise
這篇文章對于概念的分解還是比較詳細(xì)的,很不錯。里面的例子直接copy到chrome的控制臺會報錯,不過簡單修改一下就可以了。本文的例子基本上都是從這里簡單修改來的。
適用Promise的三種場景?
場景1: 級聯(lián)調(diào)用,就是幾個調(diào)用依次發(fā)生的場景。這個就是有名的回調(diào)地獄。通用的套路是,新建一個Promise,啟動流程,其他的Promise,放在級聯(lián)的then函數(shù)中。
場景2:幾個異步回調(diào)都成功,然后再進(jìn)行下一步操作,比如圖片比較大,分成幾個小圖下載,然后拼接,就是一個典型的場景。這里用Promise.all這個函數(shù)就很方便。
場景3: 幾個異步回調(diào),只要有一個成功,就可以進(jìn)行下一步。比如像主站和另外兩個備用站點(diǎn)同時請求一張圖片,只要有一個有相應(yīng),其他幾個就可以放棄。這里用Promise.race這個函數(shù)就很方便。
Promise和回調(diào)地獄是什么關(guān)系?
單個的回調(diào)函數(shù)不會形成回調(diào)地獄。串行的回調(diào),也就是上面提到的場景1,層層嵌套,導(dǎo)致代碼越套越深,結(jié)構(gòu)復(fù)雜,才形成了回調(diào)地獄。
Promise并不是替代回調(diào)函數(shù),而是對回調(diào)函數(shù)的一層封裝。比如,有名的setTimeOut函數(shù),Promise并不能替代它,只是對它進(jìn)行了一層包裝。
當(dāng)然,也不是簡單的包裝,最本質(zhì)的變化,就是將對回調(diào)函數(shù)的調(diào)用,修改成了消息發(fā)送。將對結(jié)果的處理剝離出去,交給后續(xù)的對象處理。
resolve(data); reject(error);
這兩個理解為消息發(fā)送函數(shù)更確切一點(diǎn)。一方面,將所處的Promise
的狀態(tài)由pending
改為resolved
或者reject
。另一方面,生成一個新的Promise
,并返回,同時將數(shù)據(jù)作為參數(shù)傳遞出去。Promise
包裝的函數(shù),同步和異步都是可以的,沒有本質(zhì)區(qū)別,按照一套思路去理解就可以了。同步的代碼基本不需要包裝,本來就簡單,比如用Promise.resolve(初始值)
發(fā)起一個流程。大多數(shù)情況,Promise
包裝的都是異步對象。本質(zhì)是為了把層層嵌套異步回調(diào)代碼,回調(diào)地獄 callback hell,轉(zhuǎn)變?yōu)榇械逆準(zhǔn)秸{(diào)用。Promise
對象新建的時候,狀態(tài)是Pending
,可以理解為“正在進(jìn)行中,需要等待”。
resolve(data);
一下,狀態(tài)變成了Resolved
,鏈?zhǔn)秸{(diào)用的控制權(quán)就轉(zhuǎn)移到了下一級的then(data => {callback(data)})
函數(shù)中。這里就是傳統(tǒng)的回調(diào)函數(shù)執(zhí)行的地方。
reject(error);
一下,狀態(tài)變成了Rejected
,鏈?zhǔn)秸{(diào)用的控制權(quán)就轉(zhuǎn)移到了catch(error => { })
函數(shù)中,一般建議放在最后面。這里就是集中處理錯誤的地方。
所以,不要糾結(jié)同步還是異步,將重點(diǎn)放在Promise
的對象的狀態(tài)以及鏈?zhǔn)秸{(diào)用的控制權(quán)的轉(zhuǎn)移上面。
Promise的編程范式:面向?qū)ο?or 函數(shù)式?
創(chuàng)建Promise對象,需要用到new關(guān)鍵字,并且名字也一般叫做對象。不過,Promise并不是面向?qū)ο蟮木幊蹋嗟倪€是函數(shù)式。范疇或者集合,用類來模擬。如果能夠提供一個靜態(tài)函數(shù)Promise.of來替代new關(guān)鍵字,函數(shù)式的味道就更濃厚一點(diǎn)。
鏈?zhǔn)秸{(diào)用,比較方便,要做到這一點(diǎn),每個函數(shù),比如then,catch等,都返回Promise對象,叫范疇或者集合更確切一點(diǎn)。不過,每一個Promise都是不同的,這個符合函數(shù)式編程的習(xí)慣:生成新的對象,而不是改變對象本身。之間的聯(lián)系主要是數(shù)據(jù)的傳遞,自身內(nèi)部狀態(tài)的變化。
Promise對異步調(diào)用的常用封裝套路:
function promiseFunction(resolve, reject) {
let timeOut = Math.random() * 2;
let flag = (timeOut < 1);
let callback = () => {
if (flag) {
console.log('success...');
return resolve('data: 200 OK'); // 加個return是個好習(xí)慣
} else {
console.log('fail...');
return reject('error: timeout in ' + timeOut + ' seconds.'); // 加個return是個好習(xí)慣
}
};
// start process
console.log('start async function ...');
setTimeout(callback, (timeOut * 1000));
}
function asyncFunction () {
let promise = new Promise(promiseFunction);
return promise;
}
對callback的改造:
一般的callback,應(yīng)該定義對結(jié)果的處理過程以及出錯的處理過程。Promise剝離了這些具體的處理過程,改成了發(fā)消息。成功就發(fā)送resolve(data);
失敗就發(fā)送reject(error);
這里要注意的一點(diǎn)是,resolve(data);reject(error);
,這兩個消息函數(shù)至少要用一個,當(dāng)然多用是沒關(guān)系的,否則流程就啟動不了。
resolve(data);reject(error);
之后,流程就交給后面的then或者catch來處理了,這之后的代碼都不會執(zhí)行。所以resolve(data);reject(error);
前面加個return,可以更加明確這種意圖,是個好習(xí)慣對流程函數(shù)的封裝:
一般的異步過程都分為兩步:在主線程發(fā)起異步過程,然后主線程就去做其他事情了;具體的工作一般在工作者線程中執(zhí)行。工作完成后,調(diào)用callback,通知主線程,讓主線程拿著結(jié)果做想做的事。
Promise把發(fā)起異步過程,(這里用setTimeou
t函數(shù)模擬),這個步驟封裝在一個函數(shù)中,(就是Promise構(gòu)造函數(shù)的executor參數(shù)),這個函數(shù)格式固定,參數(shù)是resolve, reject
,這里用一個名字promiseFunction把他列出來。函數(shù)式編程范式的封裝:
函數(shù)式編程一般會簡化為范疇或者集合的操作,數(shù)據(jù)和函數(shù)都包裹在一個集合容器中。
這里用Promise類的對象來模擬,這也是導(dǎo)致誤認(rèn)為面向?qū)ο缶幊痰脑颉R怨潭ㄌ茁返暮瘮?shù)(resolve, reject)作為參數(shù),通過Promise構(gòu)造函數(shù),用new關(guān)鍵字,得到了一個promise對象,完成封裝。
所以,Promise對象在構(gòu)建過程中,異步流程就已經(jīng)發(fā)起了,Promise對象的狀態(tài)就是pending===這個也是參考文章大白話講解Promise(一)中提到的注意點(diǎn)
如果不resolve或者reject一下,(throw error跟reject是同一個意思),Promise對象就一直pending,這個鏈?zhǔn)秸{(diào)用就一直停著,動不了。接口函數(shù)的封裝:
這層封裝是從軟件工程的角度,方便使用者使用的角度來做的。
函數(shù)式編程用來完成跟界面和業(yè)務(wù)無關(guān)的具體功能是比較好的,操作的也是集合。但是一般來說,業(yè)務(wù)層用面向?qū)ο蟮哪J竭M(jìn)行設(shè)計的,調(diào)用函數(shù)式編程的集合不是很方便。所以,封裝成功能型的函數(shù),(也就是上面的asyncFunction函數(shù)),用起來就比較順手了。
當(dāng)然,把生成的Promise對象return出去,是為了方便鏈?zhǔn)秸{(diào)用。
在實(shí)際使用中,可以寫得簡潔一些,上面的代碼可以精簡如下:
function asyncFunction () {
return new Promise(function(resolve, reject) {
let timeOut = Math.random() * 2;
let flag = (timeOut < 1);
// start process
console.log('start async function ...');
setTimeout(() => {
if (flag) {
console.log('success...');
return resolve('data: 200 OK'); // 加個return是個好習(xí)慣
} else {
console.log('fail...');
return reject('error: timeout in ' + timeOut + ' seconds.'); // 加個return是個好習(xí)慣
}
}, (timeOut * 1000));
});
}
Promise簡單使用的套路:
所謂簡單使用,就考慮最簡單的異步調(diào)用,(a)發(fā)起流程,等結(jié)果;(b)成功,處理結(jié)果;(c)失敗,報錯
Promise.prototype.then(callback)就是用來處理成功結(jié)果的回調(diào)函數(shù),具體的處理過程在這里定義。
then函數(shù)的第二個參數(shù)可以用來處理出錯結(jié)果,不過一般都不用。在這里處理錯誤是一種很差的方法。
then函數(shù)會返回一個Promise對象。這個前面已經(jīng)提過,這個Promise對象是then函數(shù)內(nèi)部新建的,和流程發(fā)起的那個Promise對象是不一樣的。then
函數(shù)一般建議寫同步過程,這里是執(zhí)行以往回調(diào)函數(shù)功能的地方。在流程最后,把接收到的data
再return
回去是個好習(xí)慣,萬一后面還有其他的then
要用,數(shù)據(jù)data
就可以順著節(jié)點(diǎn)傳一下,不至于中斷。
return data; 和 return Promise.resolve(data);
是等價的,內(nèi)部估計會裝換一下。所以本質(zhì)上還是return
了一個Promise
對象
如果是異步過程,建議新建一個Promise
對象包裝一下,再return
,這樣就形成了串行依賴關(guān)系。
如果什么都不return
,那么內(nèi)部會新建一個沒有值的Promise對象,相當(dāng)于return Promise.resolve(undefined);
;所以這種情況,鏈?zhǔn)秸{(diào)用還可以繼續(xù),但是參數(shù)傳遞會中斷。
then函數(shù)中一般不建議放異步過程,這樣做會增加理解的難度。下面這篇文章中就有這樣的例子:
Promise.prototype.then()
Promise.prototype.catch()
本質(zhì)上是.then(null, rejection)
的別名,這里是集中處理錯誤的地方,一般放在鏈?zhǔn)秸{(diào)用所有then的后面。這樣可以捕獲流程中的所有錯誤,包括主流程的以及后續(xù)then中出現(xiàn)的錯誤。再簡單的過程,(一般是異步過程,同步過程也一樣,比如直通),也用函數(shù)包一下,(對外的接口統(tǒng)一為函數(shù),把Promise對象隱藏起來),至少給一個then,最后跟一個catch。將原來一個整體的異步調(diào)用(流程發(fā)起,成功,失敗)轉(zhuǎn)化成了3級的鏈?zhǔn)秸{(diào)用,代碼結(jié)構(gòu)清晰很多。
比如上面通過Promise封裝好的異步函數(shù),典型的使用套路如下:
asyncFunction().then((data) => {
console.log(data);
return data; // 把數(shù)據(jù)往下傳,是個好習(xí)慣
}).catch((error) => {
console.log(error);
});
場景1: 級聯(lián)調(diào)用使用的套路:
這就是著名的回調(diào)地獄,callback hell,采用Promise包裝之后,可以改為簡潔的鏈?zhǔn)秸{(diào)用。其實(shí)就是用級聯(lián)的then來體現(xiàn)這種級聯(lián)的調(diào)用關(guān)系。
job1.then(job2).then(job3).catch(handleError);
其中,job1、job2和job3都是封裝了Promise對象的函數(shù)
注意,這里的
job1、job2和job3
都要求是一個參數(shù)的函數(shù)。因為,不論是resolve,還是reject
,傳值的參數(shù)個數(shù)都只有一個。這個可以聯(lián)想到函數(shù)式編程中的柯里化,每次只傳一個參數(shù),簡單直接。
如果想傳個參數(shù)怎么辦呢?將所有參數(shù)包裝成一個對象就可以了,resolve和reject
都是可以傳遞對象的,只是個數(shù)規(guī)定為一個而已。不過運(yùn)算的時候需要解析參數(shù),再傳值的時候需要重新組裝參數(shù),相對就麻煩一點(diǎn)了。靜態(tài)函數(shù)
Promise.resolve(data)
可以快捷地返回一個Promise
對象,一般可以用在鏈?zhǔn)秸{(diào)用的開頭,提供初始值。一般來說,在新建的
Promise
對象中發(fā)起異步流程,resolve(data)
消息發(fā)出之后,then(data => { callback(data) })
接收到數(shù)據(jù)data
,將原先的callback
在這里執(zhí)行就好了。現(xiàn)在這里不放回調(diào)代碼,而是return一個新的Promise對象,形成一個依賴鏈。
下面這個例子,就是先用Promise包裝了一個異步過程,(乘10的函數(shù));以及一個同步過程,(加100的函數(shù));用隨機(jī)數(shù)的方式,模擬過程失敗的情況。然后通過then函數(shù)級聯(lián)的方式定義依賴過程。最后用catch捕捉過程中遇到的錯誤。
Step1:用Promise封裝過程
// input*10的計算結(jié)果; setTimeout模擬異步過程;
function multiply10(input) {
return new Promise(function (resolve, reject) {
let temp = Math.random() * 1.2;
let flag = (temp < 1);
console.log('calculating ' + input + ' x ' + 10 + '...');
setTimeout(() => {
if (flag) {
return resolve(input * 10);
} else {
return reject('multiply error:' + temp);
}
}, 500);
});
}
// input+100的計算結(jié)果;同步過程
function add100(input) {
return new Promise(function (resolve, reject) {
let temp = Math.random() * 1.2;
let flag = (temp < 1);
console.log('calculating ' + input + ' + ' + 100 + '...');
if (flag) {
return resolve(input + 100);
} else {
return reject('add error:' + temp);
}
});
}
Step2:用then函數(shù)級聯(lián)的方式定義依賴過程:
// 結(jié)果是3300,或者報錯
Promise.resolve(32).then(multiply10).then(multiply10).then(add100).then(data => {
console.log('Got value: ' + data);
return data;
}).catch(error => {
console.log(error);
});
// 結(jié)果是1160,或者報錯
Promise.resolve(6).then(add100).then(multiply10).then(add100).then(data => {
console.log('Got value: ' + data);
return data;
}).catch(error => {
console.log(error);
});
// ... ... 還能寫出很多的組合情況
一般情況
then(data => { callback(data) })
函數(shù)的主要工作是接收數(shù)據(jù),然后執(zhí)行原來的回調(diào)函數(shù)。
這里一般放同步代碼;如果是異步代碼,就像上面那樣,可以新建一個Promise對象并返回,形成一個調(diào)用鏈。如果既有同步的回調(diào)代碼需要執(zhí)行,又有異步的過程需要包裝鏈接,怎么辦呢?比如上面的例子,增加顯示中間過程的功能。
可以考慮用兩個級聯(lián)的then函數(shù)分別來做這兩件事。
一個then用來執(zhí)行同步的回調(diào)函數(shù)。這里要注意將要傳遞的data return出去,不然,整個鏈?zhǔn)秸{(diào)用參數(shù)傳遞會中斷。
.then(data => {
callbacek(data);
return data; // 這里要把接收到的data傳出去,不然整個調(diào)用鏈的參數(shù)傳遞會斷掉。
})
一個then用來包裝異步過程的,并把這個新建的Promise return出去,形成異步過程依賴鏈。
.then(data => {
return new Promise(function(resolve, reject) {
let flag = ((Math.random() * 2) < 1); // demo flag
let newData = data + 1; // demo data
setTimeout(() => { // demo async function
if (flag) {
resolve(newData);
} else {
reject(new Error('error message'));
}
}, 10);
});
})
- 上面的新需求可以按照下面的套路簡單實(shí)現(xiàn):
// 結(jié)果是9880,或者報錯
Promise.resolve(888).then(add100).then(data => {
console.log('add100之后的結(jié)果為:' + data);
return data;
}).then(multiply10).then(data => {
console.log('multiply10之后的結(jié)果為:' + data);
return data;
}).then(data => {
console.log('Got value: ' + data);
return data; // 這里是最后了,不return data對流程沒影響。不過誰知道以后會不會加新的節(jié)點(diǎn),return一下還是好的。
}).catch(error => {
console.log(error);
});
場景2: 幾個異步回調(diào)都成功,然后再進(jìn)行下一步操作
場景3: 幾個異步回調(diào),只要有一個成功,就可以進(jìn)行下一步
這兩種的實(shí)現(xiàn)方式很類似,可以按照一種套路模式
只考慮異步過程,不考慮同步過程
這里用到了兩個靜態(tài)函數(shù),分別是
Promise.all()
,(場景2);Promise.race()
,(場景3);這兩個函數(shù)的參數(shù)都是一個數(shù)組,數(shù)組的成員是
Promise
對象。后面跟一個
then和catch
,就像是普通的使用場景。Promise.all()
成功時,傳遞過來的是一個結(jié)果數(shù)組;失敗時,傳遞過來的是出錯對應(yīng)的值。這里沒有鏈?zhǔn)秸{(diào)用,一長串的數(shù)據(jù)傳遞,所以這里的函數(shù)的參數(shù)個數(shù)沒有限制。不過,統(tǒng)一為一個是比較好的習(xí)慣。就算沒有參數(shù),給個空對象也可以,萬一以后要傳呢
大白話講解Promise(一)
Promise.all(),「誰跑的慢,以誰為準(zhǔn)執(zhí)行回調(diào)」;
Promise.race(),「誰跑的快,以誰為準(zhǔn)執(zhí)行回調(diào)」;
這個表述還是形象而準(zhǔn)確的。
function asyncFunction1(data = null) {
return new Promise(function(resolve, reject) {
let temp = Math.random() * 2;
let flag = (temp < 1);
// start process
console.log('start asyncfunction1 ...');
setTimeout(() => {
if (flag) {
if (data) {
return resolve(data);
} else {
return resolve('success: asyncfunction1===');
}
} else {
return reject(`fail:asyncfunction1; temp:${temp}`);
}
}, 500);
});
}
function asyncFunction2(data = null) {
return new Promise(function(resolve, reject) {
let temp = Math.random() * 2;
let flag = (temp < 1);
// start process
console.log('start asyncfunction2 ...');
setTimeout(() => {
if (flag) {
if (!data) {
return resolve(data);
} else {
return resolve('success: asyncfunction2');
}
} else {
return reject(`fail:asyncfunction2; temp:${temp}`);
}
}, 500);
});
}
// 這里傳過來的是成功結(jié)果的數(shù)組
Promise.all([asyncFunction1(), asyncFunction2()]).then(array => {
console.log(JSON.stringify(array));
return array; // 這里傳遞的是數(shù)組,比較特殊
}).catch(error => {
console.log(error);
});
// 結(jié)果是success: asyncfunction1;跑得比較快
Promise.race([asyncFunction1(), asyncFunction2()]).then(data => {
console.log(data);
return data;
}).catch(error => {
console.log(error);
});
done、finally、success、fail等其他內(nèi)容呢?
這些一些框架提供的便利方法,當(dāng)然,如果有需要,也可以自己實(shí)現(xiàn)。
上面這些是基本的使用套路,簡單直接。一個基礎(chǔ)應(yīng)用加三個典型場景,可以應(yīng)付平時大多數(shù)的異步過程。
當(dāng)然,Promise還有很多高級而靈活的用法。下面推薦幾篇文章,里面的內(nèi)容很豐富。