Promise是什么?為什么要使用?
為什么使用Promise
這篇關(guān)于promise的blog其實(shí)已經(jīng)是3年前寫(xiě)的了,但是一直在草稿狀態(tài)。因?yàn)楫?dāng)時(shí)的項(xiàng)目開(kāi)始使用ES6,我第一次接觸到promise這個(gè)概念,當(dāng)時(shí)還花了一點(diǎn)時(shí)間去理解。
現(xiàn)在每一個(gè)前端工作者肯定非常熟悉promise,它是用于處理異步的!那么,為什么要用promise呢?
首先看一個(gè)項(xiàng)目上的例子:
let submit = function(params){
validate(params, res=>{
if(res.data === "TRUE"){
submitData(params, res=>{
if(res.data === "TRUE"){
// other actions
}
})
}
})
}
以上例子,實(shí)現(xiàn)一個(gè)表單提交功能,在真正把數(shù)據(jù)提交到后臺(tái)之前,先要做一次校驗(yàn),校驗(yàn)通過(guò)才允許用戶(hù)提交。
再來(lái)看一下:
// 以下三個(gè)函數(shù)模擬異步方法
function job1(fn){
setTimeout(() => { fn("job1 success"); }, 150);
}
function job2(fn){
setTimeout(() => { fn("job2 success"); }, 200);
}
function job3(fn){
setTimeout(() => { fn("job3 success!"); }, 100);
}
(function(){
job1((res=>{ console.log(res); }));
job2((res=>{ console.log(res); }));
job3((res=>{ console.log(res); }));
})();
以上輸出:
job3 success
job1 success
job2 success
如果我們的需求是,job1, job2, job3必須按順序執(zhí)行,代碼得改成:
(function(){
job1((res=>{
console.log(res);
job2((res=>{
console.log(res);
job3((res=>{
console.log(res);
}));
}));
}));
})();
這里和上面的例子,都使用了嵌套的寫(xiě)法,如果邏輯再?gòu)?fù)雜一點(diǎn),嵌套層數(shù)會(huì)更多,容易陷入回調(diào)地獄(callback hell)。
Ajax和Node.js的回調(diào)地獄例子就非常經(jīng)典了。而promise就是為了解決這個(gè)問(wèn)題。
promise是如何處理的呢?
如果可以寫(xiě)成 job1.then(job2).then(job3)... 是不是好多了?
把異步方法修改為Promise
function job1(){
return new Promise((resolve, reject)=>{
setTimeout(() => {
console.log("job1 success");
resolve("job1 success");
}, 150);
})
}
function job2(){
return new Promise((resolve, reject)=>{
setTimeout(() => {
console.log("job2 success");
resolve("job2 success");
}, 200);
})
}
function job3(){
return new Promise((resolve, reject)=>{
setTimeout(() => {
console.log("job3 success");
resolve("job3 success");
}, 100);
})
}
這時(shí)候就可以使用鏈?zhǔn)椒椒ㄕ{(diào)用了
(function(){
job1().then(job2).then(job3).then(res=>{console.log(res);})
})();
那么,一開(kāi)始的例子也可以改寫(xiě)成
let submit = function(params){
validate(params)
.then(submitData(params))
.then(res=>{ });
}
下面,我們一起來(lái)看看Promise是怎樣實(shí)現(xiàn)的
什么是Promise
定義
Promise對(duì)象用于表示一個(gè)異步操作的最終完成 (或失敗)及其結(jié)果值。
狀態(tài)
一個(gè) Promise 必然處于這幾種狀態(tài)之一:
pending(進(jìn)行中)
fulfilled(已成功)
rejected(已失?。?/p>
狀態(tài)的變化只有兩種方法:pending變成fulfilled,pending變成rejected,狀態(tài)變化時(shí),有以下的方法來(lái)處理:
方法
then(onFulfilled, onRejected) 添加解決(fulfillment)和拒絕(rejection)回調(diào)到當(dāng)前 promise, 返回一個(gè)新的 promise, 將以回調(diào)的返回值來(lái)resolve
catch(onRejected) 添加一個(gè)拒絕(rejection) 回調(diào)到當(dāng)前 promise, 返回一個(gè)新的promise
finally(onFinally) 添加一個(gè)事件處理回調(diào)于當(dāng)前promise對(duì)象,并且在原promise對(duì)象解析完畢后,返回一個(gè)新的promise對(duì)象?;卣{(diào)會(huì)在當(dāng)前promise運(yùn)行完畢后被調(diào)用,無(wú)論當(dāng)前promise的狀態(tài)是完成(fulfilled)還是失敗(rejected)
// MDN上的例子
const myPromise =
(new Promise(myExecutorFunc))
.then(onFulfilledA,onRejectedA)
.then(onFulfilledB,onRejectedB)
.then(onFulfilledC,onRejectedC);
或者使用以下寫(xiě)法
const myPromise =
(new Promise(myExecutorFunc))
.then(onFulfilledA)
.then(onFulfilledB)
.then(onFulfilledC)
.catch(onRejectedAny);
上面的例子,就可以寫(xiě)成:
let onFulfilled = (data)=>{ console.log("Fulfilled: ", data); }
let onRejected = (error)=>{ console.log("Error: ", error); }
let onFinally = ()=>{ console.log("Finally."); }
(function(){
job1().then(job2).then(job3).then(onFulfilled)
.catch(onRejected)
.finally(onFinally);
})();
輸出:
job1 success
job2 success
job3 success
Fulfilled: job3 success
Finally.
假如其中一個(gè)job有error,那么輸出是
job1 success
job2 error
Error: job2 error
Finally.
可以看出,無(wú)論當(dāng)前promise的狀態(tài)是完成(fulfilled)還是失敗(rejected),finally()都會(huì)被調(diào)用。
再來(lái)看看另一種寫(xiě)法:
(function(){
job1()
.then(job2)
.then(job3)
.then(onFulfilled,onRejected)
.finally(onFinally);
})();
使用then(onFulfilled,onRejected) 代替catch(onRejected),輸出和以上例子一樣,所以,catch(onRejected) 其實(shí)是把then(onFulfilled,onRejected)的預(yù)留參數(shù)onFulfilled省略了,沒(méi)有本質(zhì)上的區(qū)別。
再來(lái)做一點(diǎn)修改
(function(){
job1()
.then(job2,onRejected)
.then(job3,onRejected)
.then(onFulfilled,onRejected)
.finally(onFinally);
})();
輸出:
job1 success (第二行 job1 的輸出)
job2 error (第三行 job2的輸出)
Error: job2 error (第四行 onRejected 的輸出)
Fulfilled: undefined (第五行 onFulfilled 的輸出)
Finally. (第六行 onFinally 的輸出)
job2的promise調(diào)用了reject方法,狀態(tài)變成rejected,所以在then()的時(shí)候調(diào)用了onRejected,但是promise的方法都會(huì)返回一個(gè)新的promise,所以在第五行的時(shí)候,then()對(duì)應(yīng)的promise是上一行onRejected()返回的promise, 會(huì)調(diào)用onFulfilled()
任何不是 throw 的終止都會(huì)創(chuàng)建一個(gè)"已決議(resolved)"狀態(tài),而以 throw 終止則會(huì)創(chuàng)建一個(gè)"已拒絕"狀態(tài)。
如果我們把onRejected()修改一下
let onRejected = (error)=>{
console.log("Error: ", error);
throw new Error(error);
}
那么,上面的輸出就變成:
job1 success (第二行 job1 的輸出)
job2 error (第三行 job2的輸出)
Error: job2 error (第四行 onRejected 的輸出)
Error: Error: job2 error (第五行 onRejected 的輸出) *
at onRejected (.../test.js:34:9)
at processTicksAndRejections (internal/process/task_queues.js:93:5)
Finally. (第六行 onFinally 的輸出)*
靜態(tài)方法
有一個(gè)使用得比較多的方法是Promise.all(),先來(lái)看代碼
(function() {
let p1 = job1();
let p2 = job2();
let p3 = job3();
Promise.all([p1, p2, p3]).then(values=>{
console.log(values); //
})
})();
輸出:
job3 success
job1 success
job2 success
[ 'job1 success', 'job2 success', 'job3 success' ]
Promise.all()方法接收一個(gè)promise的iterable類(lèi)型(注:Array,Map,Set都屬于ES6的iterable類(lèi)型)的輸入,并且只返回一個(gè)Promise實(shí)例, 那個(gè)輸入的所有promise的resolve回調(diào)的結(jié)果是一個(gè)數(shù)組。
但是這里注意一下,和上面的對(duì)比,job1、job2、job3不是按順序執(zhí)行的。
我們是不是還可能用上面then(onFulfilled,onRejected)或者catch(onRejected)來(lái)使用呢?
(function() {
let p1 = job1();
let p2 = job2();
let p3 = job3();
Promise.all([p1, p2, p3]).then(onFulfilled, onRejected).finally(onFinally);
})();
// 或者
(function() {
let p1 = job1();
let p2 = job2();
let p3 = job3();
Promise.all([p1, p2, p3]).then(onFulfilled).catch(onRejected).finally(onFinally);
})();
輸入都是:
Error: job2 error
Finally.
Promise.all 在任意一個(gè)傳入的 promise 失敗時(shí)返回失敗。
因?yàn)閖ob2的狀態(tài)是失敗了,所以最后調(diào)用的是onRejected
Promise與事件循環(huán)
當(dāng)涉及異步事件的時(shí)候,事件循環(huán)就成是了個(gè)很讓人頭大的問(wèn)題。先來(lái)看看概念:
- 宏任務(wù)
- 主代碼塊
- setTimeout
- setInterval
- setImmediate ()-Node
- requestAnimationFrame ()-瀏覽器
- 微任務(wù)
- process.nextTick ()-Node
- Promise.then()
- catch
- finally
- Object.observe
- MutationObserver
為了更好了看出執(zhí)行順序,我們先來(lái)修改一下上面的job的定義
function job1(){
return new Promise((resolve, reject)=>{
console.log("job1 start...")
setTimeout(() => {
console.log("job1 success");
resolve(1);
}, 150); //定時(shí)器,150ms后執(zhí)行
})
}
function job2(){
return new Promise((resolve, reject)=>{
console.log("job2 start...")
setTimeout(() => {
console.log("job2 success");
resolve(2);
}, 100); //定時(shí)器,100ms后執(zhí)行
})
}
function job3(){
return new Promise((resolve, reject)=>{
console.log("job3 start...")
setTimeout(() => {
console.log("job3 success");
resolve(3);
},0);
})
}
調(diào)用方法如下
console.log("***** START ******");
let p1 = job1();
let p2 = p1.then(job2);
let p3 = p2.then(job3);
let p = p3.then(onFulfilled);
console.log(p1, p2, p3, p);
setTimeout(() => {
console.log('500ms: the stack is now empty');
console.log(p1, p2, p3, p);
},500);
setTimeout(() => {
console.log('0ms...');
},0);
setTimeout(() => {
console.log('250ms...');
},250);
console.log("***** END ******");
輸入順序會(huì)是怎樣呢?
分析:
根據(jù)事件循環(huán),
- 先執(zhí)行同步方法
console.log("***** START ******"); - 構(gòu)造函數(shù)
new Promise()是同步任務(wù),所以執(zhí)行 job1的console.log("job1 start...") - 遇到
setTimeout,移交給定時(shí)器線(xiàn)程,150ms后放入宏任務(wù)隊(duì)列,到此job1結(jié)束 - 接下都是
Promise.then()的方法,是異步微任務(wù),放入微任務(wù)隊(duì)列 - 執(zhí)行
console.log(p1, p2, p3, p);,這時(shí),promise的狀態(tài)都是pending - 遇到
setTimeout,移交給定時(shí)器線(xiàn)程,500ms后放入宏任務(wù)隊(duì)列 - 遇到
setTimeout,移交給定時(shí)器線(xiàn)程,0ms后放入宏任務(wù)隊(duì)列(即使是0,但是仍然要按規(guī)矩) - 遇到
setTimeout,移交給定時(shí)器線(xiàn)程,250ms后放入宏任務(wù)隊(duì)列 - 執(zhí)行
console.log("***** END ******"),到這里主線(xiàn)程執(zhí)行完畢 - 開(kāi)始執(zhí)行任務(wù)隊(duì)列,
宏任務(wù)隊(duì)列中根據(jù)時(shí)間順序: [0ms, 200ms,250ms, 500ms]
a. 執(zhí)行console.log('0ms...');
b. 執(zhí)行console.log("job1 success");和resolve(1);
c. 執(zhí)行console.log('250ms...');
d. 執(zhí)行console.log('500ms: the stack is now empty'');和console.log(p1, p2, p3, p);
但是這里注意一下,當(dāng)一個(gè)宏任務(wù)執(zhí)行完,會(huì)在渲染前,將執(zhí)行期間所產(chǎn)生的所有微任務(wù)都執(zhí)行完 。b任務(wù)執(zhí)行完的時(shí)候,p1.then(job2)會(huì)執(zhí)行,即會(huì)執(zhí)行console.log("job2 start..."),但是由于job2中也有setTimeout,根據(jù)時(shí)間放入宏任務(wù)隊(duì)列
最后輸出:
***** START ******
job1 start...
Promise { <pending> } Promise { <pending> } Promise { <pending> } Promise { <pending> }
***** END ******
0ms...
job1 success
job2 start...
job2 success
job3 start...
250ms...
job3 success
Fulfilled: 3
500ms: the stack is now empty
Promise { 1 } Promise { 2 } Promise { 3 } Promise { 'Completed!' }
最后所有promise都是fulfilled/rejected狀態(tài)
Promise.all()的同步和異步
如果使用Promise.all()呢?
console.log("***** START ******");
let p1 = job1();
let p3 = job3();
let p2 = job2();
let p = Promise.all([p1, p2, p3]);
let ep = Promise.all([]);
console.log(p1, p2, p3);
console.log(ep, p);
setTimeout(() => {
console.log('the stack is now empty');
console.log(p1, p2, p3, p);
},500);
setTimeout(() => {
console.log('0ms...');
},0);
console.log("***** END ******")
結(jié)果:
***** START ******
job1 start...
job3 start...
job2 start...
Promise { <pending> } Promise { <pending> } Promise { <pending> }
Promise { [] } Promise { <pending> }
***** END ******
job3 success
0ms...
job2 success
job1 success
the stack is now empty
Promise { 1 } Promise { 2 } Promise { 3 } Promise { [ 1, 2, 3 ] }
這里有一個(gè)注意點(diǎn):
Promise.all當(dāng)且僅當(dāng)傳入的可迭代對(duì)象為空時(shí)為同步
所以最開(kāi)始的時(shí)候,console.log(ep, p);的輸出一個(gè)是fulfilled,一個(gè)是pending
async/await
最后順便看看 ES2017新增的 async/await吧
await關(guān)鍵字接收一個(gè)promise并獎(jiǎng)其轉(zhuǎn)換為一個(gè)返回值或拋出一個(gè)異常
async關(guān)鍵字意味著函數(shù)返回一個(gè)promise
任何使用
await的代碼都是異步的,只能在async關(guān)鍵字聲明的函數(shù)內(nèi)部使用await關(guān)鍵字
上面的例子,如果想要取出每一步的結(jié)果,可能會(huì)比較麻煩,可以改寫(xiě)成
async function run() {
// 按順序執(zhí)行
let r1 = await job1();
let r2 = await job2();
let r3 = await job3();
console.log(r1,r2, r3);
}
// output: 1 2 3
或使用Promise.all
async function run() {
// 不會(huì)按順序執(zhí)行
let [r1,r2, r3] = await Promise.all([job1(), job2(), job3()]);
console.log(r1,r2, r3);
}
// output: 1 2 3
參考文章:
HTML Standard
MDN上的說(shuō)明
Promise+
講JS運(yùn)行機(jī)制,事件循環(huán)講得很清晰