Promise
是什么伯病?為什么要使用?
為什么使用Promise
這篇關(guān)于promise
的blog其實(shí)已經(jīng)是3年前寫的了,但是一直在草稿狀態(tài)训桶。因?yàn)楫?dāng)時(shí)的項(xiàng)目開始使用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)通過才允許用戶提交耕漱。
再來看一下:
// 以下三個(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);
}));
}));
}));
})();
這里和上面的例子螟够,都使用了嵌套的寫法,如果邏輯再復(fù)雜一點(diǎn)峡钓,嵌套層數(shù)會(huì)更多妓笙,容易陷入回調(diào)地獄(callback hell)
。
Ajax
和Node.js
的回調(diào)地獄
例子就非常經(jīng)典了能岩。而promise
就是為了解決這個(gè)問題寞宫。
promise
是如何處理的呢?
如果可以寫成 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);})
})();
那么拉鹃,一開始的例子也可以改寫成
let submit = function(params){
validate(params)
.then(submitData(params))
.then(res=>{ });
}
下面辈赋,我們一起來看看Promise是怎樣實(shí)現(xiàn)的
什么是Promise
定義
Promise
對(duì)象用于表示一個(gè)異步操作的最終完成 (或失敗)及其結(jié)果值鲫忍。
狀態(tài)
一個(gè) Promise 必然處于這幾種狀態(tài)之一:
pending
(進(jìn)行中)
fulfilled
(已成功)
rejected
(已失敗)
狀態(tài)的變化只有兩種方法:pending
變成fulfilled
钥屈,pending
變成rejected
悟民,狀態(tài)變化時(shí),有以下的方法來處理:
方法
then(onFulfilled, onRejected)
添加解決(fulfillment)和拒絕(rejection)回調(diào)到當(dāng)前 promise, 返回一個(gè)新的 promise, 將以回調(diào)的返回值來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)用智润,無論當(dāng)前promise的狀態(tài)是完成(fulfilled)還是失敗(rejected)
// MDN上的例子
const myPromise =
(new Promise(myExecutorFunc))
.then(onFulfilledA,onRejectedA)
.then(onFulfilledB,onRejectedB)
.then(onFulfilledC,onRejectedC);
或者使用以下寫法
const myPromise =
(new Promise(myExecutorFunc))
.then(onFulfilledA)
.then(onFulfilledB)
.then(onFulfilledC)
.catch(onRejectedAny);
上面的例子,就可以寫成:
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.
可以看出窟绷,無論當(dāng)前promise的狀態(tài)是完成(fulfilled)
還是失敗(rejected)
,finally()
都會(huì)被調(diào)用鼎姐。
再來看看另一種寫法:
(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
省略了,沒有本質(zhì)上的區(qū)別肯腕。
再來做一點(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()
负敏,先來看代碼
(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
類型(注:Array
贡茅,Map
,Set
都屬于ES6的iterable
類型)的輸入其做,并且只返回一個(gè)Promise
實(shí)例顶考, 那個(gè)輸入的所有promise的resolve回調(diào)的結(jié)果是一個(gè)數(shù)組。
但是這里注意一下妖泄,和上面的對(duì)比驹沿,job1、job2蹈胡、job3不是按順序執(zhí)行的渊季。
我們是不是還可能用上面then(onFulfilled,onRejected)
或者catch(onRejected)
來使用呢朋蔫?
(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ù)
- 主代碼塊
- setTimeout
- setInterval
- setImmediate ()-Node
- requestAnimationFrame ()-瀏覽器
- 微任務(wù)
- process.nextTick ()-Node
- Promise.then()
- catch
- finally
- Object.observe
- MutationObserver
為了更好了看出執(zhí)行順序病涨,我們先來修改一下上面的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í)器線程
励两,150ms后放入宏任務(wù)隊(duì)列
,到此job1結(jié)束 - 接下都是
Promise.then()
的方法囊颅,是異步微任務(wù)当悔,放入微任務(wù)隊(duì)列
- 執(zhí)行
console.log(p1, p2, p3, p);
,這時(shí)踢代,promise的狀態(tài)都是pending
- 遇到
setTimeout
盲憎,移交給定時(shí)器線程
,500ms后放入宏任務(wù)隊(duì)列
- 遇到
setTimeout
胳挎,移交給定時(shí)器線程
饼疙,0ms后放入宏任務(wù)隊(duì)列
(即使是0,但是仍然要按規(guī)矩) - 遇到
setTimeout
慕爬,移交給定時(shí)器線程
窑眯,250ms后放入宏任務(wù)隊(duì)列
- 執(zhí)行
console.log("***** END ******")
,到這里主線程執(zhí)行完畢 - 開始執(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í)為同步
所以最開始的時(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ì)比較麻煩,可以改寫成
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上的說明
Promise+
講JS運(yùn)行機(jī)制题画,事件循環(huán)講得很清晰