關于Promise的基本內(nèi)容初狰,已經(jīng)寫過一篇文章莫杈。基本的會用跷究,偶爾也用過姓迅,不過知識點比較多,并且代碼在外面套了好幾層俊马,感覺也比較復雜丁存,一直理不順頭緒。最近看了下面鏈接的幾篇文章柴我,感覺很不錯解寝,對于Promise基本概念的理解比以前要清晰一點了。
Promise的知識點很多艘儒,這里是一次從多到少的收斂過程聋伦,寫了幾點平時可能會用到的最基礎的用法夫偶。
大白話講解Promise(一)
這篇文章寫得還是比較簡單直接的,對于理解概念很有幫助觉增,推薦好好看看兵拢。
廖雪峰的Promise
這篇文章對于概念的分解還是比較詳細的,很不錯逾礁。里面的例子直接copy到chrome的控制臺會報錯说铃,不過簡單修改一下就可以了。本文的例子基本上都是從這里簡單修改來的嘹履。
適用Promise的三種場景腻扇?
場景1: 級聯(lián)調(diào)用,就是幾個調(diào)用依次發(fā)生的場景砾嫉。這個就是有名的回調(diào)地獄幼苛。通用的套路是,新建一個Promise焕刮,啟動流程舶沿,其他的Promise,放在級聯(lián)的then函數(shù)中济锄。
場景2:幾個異步回調(diào)都成功暑椰,然后再進行下一步操作霍转,比如圖片比較大荐绝,分成幾個小圖下載,然后拼接避消,就是一個典型的場景低滩。這里用Promise.all這個函數(shù)就很方便。
場景3: 幾個異步回調(diào)岩喷,只要有一個成功恕沫,就可以進行下一步。比如像主站和另外兩個備用站點同時請求一張圖片纱意,只要有一個有相應婶溯,其他幾個就可以放棄。這里用Promise.race這個函數(shù)就很方便偷霉。
Promise和回調(diào)地獄是什么關系迄委?
單個的回調(diào)函數(shù)不會形成回調(diào)地獄。串行的回調(diào)类少,也就是上面提到的場景1叙身,層層嵌套,導致代碼越套越深硫狞,結構復雜信轿,才形成了回調(diào)地獄晃痴。
Promise并不是替代回調(diào)函數(shù),而是對回調(diào)函數(shù)的一層封裝财忽。比如倘核,有名的setTimeOut函數(shù),Promise并不能替代它即彪,只是對它進行了一層包裝笤虫。
當然,也不是簡單的包裝祖凫,最本質(zhì)的變化琼蚯,就是將對回調(diào)函數(shù)的調(diào)用,修改成了消息發(fā)送惠况。將對結果的處理剝離出去遭庶,交給后續(xù)的對象處理。
resolve(data); reject(error);
這兩個理解為消息發(fā)送函數(shù)更確切一點稠屠。一方面峦睡,將所處的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)榇械逆準秸{(diào)用渐尿。Promise
對象新建的時候醉途,狀態(tài)是Pending
,可以理解為“正在進行中涡戳,需要等待”结蟋。
resolve(data);
一下,狀態(tài)變成了Resolved
渔彰,鏈式調(diào)用的控制權就轉(zhuǎn)移到了下一級的then(data => {callback(data)})
函數(shù)中嵌屎。這里就是傳統(tǒng)的回調(diào)函數(shù)執(zhí)行的地方推正。
reject(error);
一下,狀態(tài)變成了Rejected
宝惰,鏈式調(diào)用的控制權就轉(zhuǎn)移到了catch(error => { })
函數(shù)中植榕,一般建議放在最后面。這里就是集中處理錯誤的地方尼夺。
所以尊残,不要糾結同步還是異步,將重點放在Promise
的對象的狀態(tài)以及鏈式調(diào)用的控制權的轉(zhuǎn)移上面淤堵。
Promise的編程范式:面向?qū)ο?or 函數(shù)式寝衫?
創(chuàng)建Promise對象,需要用到new關鍵字拐邪,并且名字也一般叫做對象慰毅。不過,Promise并不是面向?qū)ο蟮木幊淘祝嗟倪€是函數(shù)式汹胃。范疇或者集合,用類來模擬东臀。如果能夠提供一個靜態(tài)函數(shù)Promise.of來替代new關鍵字着饥,函數(shù)式的味道就更濃厚一點。
鏈式調(diào)用惰赋,比較方便宰掉,要做到這一點,每個函數(shù)谤逼,比如then贵扰,catch等仇穗,都返回Promise對象流部,叫范疇或者集合更確切一點。不過纹坐,每一個Promise都是不同的枝冀,這個符合函數(shù)式編程的習慣:生成新的對象,而不是改變對象本身耘子。之間的聯(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是個好習慣
} else {
console.log('fail...');
return reject('error: timeout in ' + timeOut + ' seconds.'); // 加個return是個好習慣
}
};
// start process
console.log('start async function ...');
setTimeout(callback, (timeOut * 1000));
}
function asyncFunction () {
let promise = new Promise(promiseFunction);
return promise;
}
對callback的改造:
一般的callback谷誓,應該定義對結果的處理過程以及出錯的處理過程绒障。Promise剝離了這些具體的處理過程,改成了發(fā)消息捍歪。成功就發(fā)送resolve(data)户辱;
失敗就發(fā)送reject(error);
這里要注意的一點是,resolve(data);reject(error);
才顿,這兩個消息函數(shù)至少要用一個返奉,當然多用是沒關系的,否則流程就啟動不了必逆。
resolve(data)怠堪;reject(error);
之后,流程就交給后面的then或者catch來處理了名眉,這之后的代碼都不會執(zhí)行粟矿。所以resolve(data);reject(error);
前面加個return损拢,可以更加明確這種意圖嚷炉,是個好習慣對流程函數(shù)的封裝:
一般的異步過程都分為兩步:在主線程發(fā)起異步過程,然后主線程就去做其他事情了探橱;具體的工作一般在工作者線程中執(zhí)行申屹。工作完成后,調(diào)用callback隧膏,通知主線程哗讥,讓主線程拿著結果做想做的事。
Promise把發(fā)起異步過程胞枕,(這里用setTimeou
t函數(shù)模擬)杆煞,這個步驟封裝在一個函數(shù)中,(就是Promise構造函數(shù)的executor參數(shù))腐泻,這個函數(shù)格式固定决乎,參數(shù)是resolve, reject
,這里用一個名字promiseFunction把他列出來派桩。函數(shù)式編程范式的封裝:
函數(shù)式編程一般會簡化為范疇或者集合的操作构诚,數(shù)據(jù)和函數(shù)都包裹在一個集合容器中。
這里用Promise類的對象來模擬铆惑,這也是導致誤認為面向?qū)ο缶幊痰脑蚍吨觥R怨潭ㄌ茁返暮瘮?shù)(resolve, reject)作為參數(shù),通過Promise構造函數(shù)员魏,用new關鍵字丑蛤,得到了一個promise對象,完成封裝撕阎。
所以受裹,Promise對象在構建過程中,異步流程就已經(jīng)發(fā)起了虏束,Promise對象的狀態(tài)就是pending===這個也是參考文章大白話講解Promise(一)中提到的注意點
如果不resolve或者reject一下棉饶,(throw error跟reject是同一個意思)脑慧,Promise對象就一直pending,這個鏈式調(diào)用就一直停著砰盐,動不了闷袒。接口函數(shù)的封裝:
這層封裝是從軟件工程的角度,方便使用者使用的角度來做的岩梳。
函數(shù)式編程用來完成跟界面和業(yè)務無關的具體功能是比較好的囊骤,操作的也是集合。但是一般來說冀值,業(yè)務層用面向?qū)ο蟮哪J竭M行設計的也物,調(diào)用函數(shù)式編程的集合不是很方便。所以列疗,封裝成功能型的函數(shù)滑蚯,(也就是上面的asyncFunction函數(shù)),用起來就比較順手了抵栈。
當然告材,把生成的Promise對象return出去,是為了方便鏈式調(diào)用古劲。
在實際使用中斥赋,可以寫得簡潔一些,上面的代碼可以精簡如下:
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是個好習慣
} else {
console.log('fail...');
return reject('error: timeout in ' + timeOut + ' seconds.'); // 加個return是個好習慣
}
}, (timeOut * 1000));
});
}
Promise簡單使用的套路:
所謂簡單使用产艾,就考慮最簡單的異步調(diào)用疤剑,(a)發(fā)起流程,等結果闷堡;(b)成功隘膘,處理結果;(c)失敗杠览,報錯
Promise.prototype.then(callback)就是用來處理成功結果的回調(diào)函數(shù)弯菊,具體的處理過程在這里定義。
then函數(shù)的第二個參數(shù)可以用來處理出錯結果倦零,不過一般都不用误续。在這里處理錯誤是一種很差的方法。
then函數(shù)會返回一個Promise對象扫茅。這個前面已經(jīng)提過,這個Promise對象是then函數(shù)內(nèi)部新建的育瓜,和流程發(fā)起的那個Promise對象是不一樣的葫隙。then
函數(shù)一般建議寫同步過程,這里是執(zhí)行以往回調(diào)函數(shù)功能的地方躏仇。在流程最后恋脚,把接收到的data
再return
回去是個好習慣腺办,萬一后面還有其他的then
要用,數(shù)據(jù)data
就可以順著節(jié)點傳一下糟描,不至于中斷怀喉。
return data; 和 return Promise.resolve(data);
是等價的,內(nèi)部估計會裝換一下船响。所以本質(zhì)上還是return
了一個Promise
對象
如果是異步過程躬拢,建議新建一個Promise
對象包裝一下,再return
见间,這樣就形成了串行依賴關系聊闯。
如果什么都不return
,那么內(nèi)部會新建一個沒有值的Promise對象米诉,相當于return Promise.resolve(undefined);
菱蔬;所以這種情況,鏈式調(diào)用還可以繼續(xù)史侣,但是參數(shù)傳遞會中斷拴泌。
then函數(shù)中一般不建議放異步過程,這樣做會增加理解的難度惊橱。下面這篇文章中就有這樣的例子:
Promise.prototype.then()
Promise.prototype.catch()
本質(zhì)上是.then(null, rejection)
的別名弛针,這里是集中處理錯誤的地方,一般放在鏈式調(diào)用所有then的后面李皇。這樣可以捕獲流程中的所有錯誤削茁,包括主流程的以及后續(xù)then中出現(xiàn)的錯誤。再簡單的過程掉房,(一般是異步過程茧跋,同步過程也一樣,比如直通)卓囚,也用函數(shù)包一下瘾杭,(對外的接口統(tǒng)一為函數(shù),把Promise對象隱藏起來)哪亿,至少給一個then粥烁,最后跟一個catch。將原來一個整體的異步調(diào)用(流程發(fā)起蝇棉,成功讨阻,失敗)轉(zhuǎn)化成了3級的鏈式調(diào)用篡殷,代碼結構清晰很多钝吮。
比如上面通過Promise封裝好的異步函數(shù),典型的使用套路如下:
asyncFunction().then((data) => {
console.log(data);
return data; // 把數(shù)據(jù)往下傳,是個好習慣
}).catch((error) => {
console.log(error);
});
場景1: 級聯(lián)調(diào)用使用的套路:
這就是著名的回調(diào)地獄奇瘦,callback hell棘催,采用Promise包裝之后,可以改為簡潔的鏈式調(diào)用耳标。其實就是用級聯(lián)的then來體現(xiàn)這種級聯(lián)的調(diào)用關系醇坝。
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ī)定為一個而已。不過運算的時候需要解析參數(shù)橙凳,再傳值的時候需要重新組裝參數(shù)蕾殴,相對就麻煩一點了。靜態(tài)函數(shù)
Promise.resolve(data)
可以快捷地返回一個Promise
對象岛啸,一般可以用在鏈式調(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ù))恢着;用隨機數(shù)的方式桐愉,模擬過程失敗的情況。然后通過then函數(shù)級聯(lián)的方式定義依賴過程掰派。最后用catch捕捉過程中遇到的錯誤从诲。
Step1:用Promise封裝過程
// input*10的計算結果; 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的計算結果靡羡;同步過程
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)的方式定義依賴過程:
// 結果是3300系洛,或者報錯
Promise.resolve(32).then(multiply10).then(multiply10).then(add100).then(data => {
console.log('Got value: ' + data);
return data;
}).catch(error => {
console.log(error);
});
// 結果是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出去,不然雷恃,整個鏈式調(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);
});
})
- 上面的新需求可以按照下面的套路簡單實現(xiàn):
// 結果是9880唱凯,或者報錯
Promise.resolve(888).then(add100).then(data => {
console.log('add100之后的結果為:' + data);
return data;
}).then(multiply10).then(data => {
console.log('multiply10之后的結果為:' + data);
return data;
}).then(data => {
console.log('Got value: ' + data);
return data; // 這里是最后了,不return data對流程沒影響谎痢。不過誰知道以后會不會加新的節(jié)點磕昼,return一下還是好的。
}).catch(error => {
console.log(error);
});
場景2: 幾個異步回調(diào)都成功节猿,然后再進行下一步操作
場景3: 幾個異步回調(diào)票从,只要有一個成功漫雕,就可以進行下一步
這兩種的實現(xiàn)方式很類似,可以按照一種套路模式
只考慮異步過程峰鄙,不考慮同步過程
這里用到了兩個靜態(tài)函數(shù)浸间,分別是
Promise.all()
,(場景2)吟榴;Promise.race()
魁蒜,(場景3);這兩個函數(shù)的參數(shù)都是一個數(shù)組吩翻,數(shù)組的成員是
Promise
對象兜看。后面跟一個
then和catch
,就像是普通的使用場景狭瞎。Promise.all()
成功時细移,傳遞過來的是一個結果數(shù)組;失敗時熊锭,傳遞過來的是出錯對應的值弧轧。這里沒有鏈式調(diào)用,一長串的數(shù)據(jù)傳遞球涛,所以這里的函數(shù)的參數(shù)個數(shù)沒有限制劣针。不過,統(tǒng)一為一個是比較好的習慣亿扁。就算沒有參數(shù)捺典,給個空對象也可以,萬一以后要傳呢
大白話講解Promise(一)
Promise.all()从祝,「誰跑的慢襟己,以誰為準執(zhí)行回調(diào)」;
Promise.race(),「誰跑的快牍陌,以誰為準執(zhí)行回調(diào)」;
這個表述還是形象而準確的擎浴。
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);
});
}
// 這里傳過來的是成功結果的數(shù)組
Promise.all([asyncFunction1(), asyncFunction2()]).then(array => {
console.log(JSON.stringify(array));
return array; // 這里傳遞的是數(shù)組,比較特殊
}).catch(error => {
console.log(error);
});
// 結果是success: asyncfunction1毒涧;跑得比較快
Promise.race([asyncFunction1(), asyncFunction2()]).then(data => {
console.log(data);
return data;
}).catch(error => {
console.log(error);
});
done贮预、finally、success契讲、fail等其他內(nèi)容呢仿吞?
這些一些框架提供的便利方法,當然捡偏,如果有需要唤冈,也可以自己實現(xiàn)。
上面這些是基本的使用套路银伟,簡單直接你虹。一個基礎應用加三個典型場景绘搞,可以應付平時大多數(shù)的異步過程。
當然傅物,Promise還有很多高級而靈活的用法夯辖。下面推薦幾篇文章,里面的內(nèi)容很豐富挟伙。