摘要:一是如何鏈?zhǔn)秸{(diào)用,二是如何中止鏈?zhǔn)秸{(diào)用。到目前為止,我們就基本了解了的用法及特點(diǎn),并實(shí)現(xiàn)用重構(gòu)用回調(diào)函數(shù)寫(xiě)的異步操作。
Abstract
本文主要講的是如何實(shí)現(xiàn) Promise 的鏈?zhǔn)秸{(diào)用。也就是 promise().then().then().catch() 的形式,然后討論如何在某一個(gè) then() 里面中止 Promise。
在程序中,只要返回了一個(gè) promise 對(duì)象,如果 promise 對(duì)象不是 Rejected 或 Fulfilled 狀態(tài),then 方法就會(huì)繼續(xù)調(diào)用。利用這個(gè)特性,可以處理多個(gè)異步邏輯。但有時(shí)候某個(gè) then 方法的執(zhí)行結(jié)果可能會(huì)決定是否需要執(zhí)行下一個(gè) then,這個(gè)時(shí)候就需中止 promise,主要思想就是使用 reject 來(lái)中止 promise 的 then 繼續(xù)執(zhí)行。
“中止”這個(gè)詞不知道用得是否準(zhǔn)確。這里可能還是 break 的含義更精確,跳出本次 promise,不繼續(xù)執(zhí)行后面的 then 方法。但 promise 依舊會(huì)繼續(xù)執(zhí)行。
Can I use promises當(dāng)前瀏覽器對(duì) Promise 的支持情況見(jiàn)下圖:
http://caniuse.com/#search=promise
Promise先簡(jiǎn)單復(fù)習(xí)一下 Promise。Promise 其實(shí)很簡(jiǎn)單,就是一個(gè)處理異步的方法。一般可以通過(guò) new 方法來(lái)調(diào)用 Promise 的構(gòu)造器實(shí)例化一個(gè) promise 對(duì)象:
var promise = new Promise((resolve, reject) => { // 異步處理 // 處理結(jié)束后,調(diào)用 resolve 或 reject // 成功時(shí)就調(diào)用 resolve // 失敗時(shí)就調(diào)用 reject });
用 new Promise 實(shí)例化的 promise 對(duì)象有以下三個(gè)狀態(tài):
"has-resolution" - Fulfilled。resolve(成功)時(shí),此時(shí)會(huì)調(diào)用 onFulfilled
"has-rejection" - Rejected。reject(失敗)時(shí),此時(shí)會(huì)調(diào)用 onRejected
"unresolved" - Pending。既不是resolve也不是reject的狀態(tài),也就是promise對(duì)象剛被創(chuàng)建后的初始化狀態(tài)等
關(guān)于上面這三種狀態(tài)的讀法,其中左側(cè)為在 ES6 Promises 規(guī)范中定義的術(shù)語(yǔ), 而右側(cè)則是在 Promises/A+ 中描述狀態(tài)的術(shù)語(yǔ)。基本上狀態(tài)在代碼中是不會(huì)涉及到的,所以名稱(chēng)也無(wú)需太在意。
Promise Chain先來(lái)假設(shè)一個(gè)業(yè)務(wù)需求:在系統(tǒng)中使用教務(wù)系統(tǒng)賬號(hào)進(jìn)行登錄。首先用戶(hù)在登錄頁(yè)面輸入用戶(hù)名(教務(wù)系統(tǒng)賬號(hào))和密碼(教務(wù)系統(tǒng)密碼);然后判斷數(shù)據(jù)庫(kù)中是否存在該用戶(hù);如果不存在則使用用戶(hù)名和密碼模擬登錄教務(wù)系統(tǒng),如果模擬登錄成功,則存儲(chǔ)用戶(hù)名和密碼,并返回登錄成功。
聽(tīng)起來(lái)就有點(diǎn)復(fù)雜對(duì)不對(duì)?于是畫(huà)了個(gè)流程圖來(lái)解釋整個(gè)業(yè)務(wù)邏輯:
上圖只是一個(gè)簡(jiǎn)化版本,比如密碼加密、session設(shè)置等沒(méi)有表現(xiàn)出來(lái),大家知道就好。圖中 (1)、(2)、(3) 三個(gè)地方就是會(huì)進(jìn)行異步處理的地方,一般數(shù)據(jù)庫(kù)操作、網(wǎng)絡(luò)請(qǐng)求都是異步的。
如果用傳統(tǒng)的回調(diào)函數(shù) callback 來(lái)處理上面的邏輯,嵌套的層級(jí)就會(huì)比較深,上面的業(yè)務(wù)因?yàn)橛腥齻€(gè)異步操作所以有三層回調(diào),代碼大概會(huì)是下面的樣子:
// 根據(jù) name 查詢(xún)用戶(hù)信息 findUserByName(name, function(err, userinfo) { if (err) { return res.json({ code: 1000, message: "查詢(xún)用戶(hù)信息,數(shù)據(jù)庫(kù)操作數(shù)出現(xiàn)異常", }); } if (userinfo.length > 0) { // 用戶(hù)存在 if (userinfo[0].pwd === pwd) // 密碼正確 return res.json({ code: 0, message: "登錄成功", }); } // 數(shù)據(jù)庫(kù)中不存在該用戶(hù),模擬登錄教務(wù)系統(tǒng) loginEducationSystem(name, pwd, function(err, result) { if (err) { return res.json({ code: 1001, message: "模擬登錄教務(wù)系統(tǒng)出現(xiàn)異常", }); } // 約定正確情況下,code 為 0 if (result.code !== 0) { return res.json({ code: 1002, message: "模擬登錄教務(wù)系統(tǒng)失敗,可能是用戶(hù)名或密碼錯(cuò)誤", }); } // 模擬登錄成功,將用戶(hù)名密碼存入數(shù)據(jù)庫(kù) saveUserToDB(name, pwd, function(err, result) { if (err) { return res.json({ code: 1003, message: "將用戶(hù)名密碼存入數(shù)據(jù)庫(kù)出現(xiàn)異常", }); } if (result.code !== 0) { return res.json({ code: 1004, message: "將用戶(hù)名密碼存入數(shù)據(jù)庫(kù)出現(xiàn)異常", }); } return res.json({ code: 0, message: "登錄成功!", }); }); }); });
上面的代碼可能存在的不優(yōu)雅之處:
隨著業(yè)務(wù)邏輯變負(fù)責(zé),回調(diào)層級(jí)會(huì)越來(lái)越深
代碼耦合度比較高,不易修改
每一步操作都需要手動(dòng)進(jìn)行異常處理,比較麻煩
接下來(lái)再用 promise 實(shí)現(xiàn)此處的業(yè)務(wù)需求。使用 promise 編碼之前,可以先思考兩個(gè)問(wèn)題。
一是如何鏈?zhǔn)秸{(diào)用,二是如何中止鏈?zhǔn)秸{(diào)用。
How to Use Promise Chain業(yè)務(wù)中有三個(gè)需要異步處理的功能,所以會(huì)分別實(shí)例化三個(gè) promise 對(duì)象,然后對(duì) promise 進(jìn)行鏈?zhǔn)秸{(diào)用。那么,如何進(jìn)行鏈?zhǔn)秸{(diào)用?
其實(shí)也很簡(jiǎn)單,直接在 promise 的 then 方法里面返回另一個(gè) promise 即可。例如:
function start() { return new Promise((resolve, reject) => { resolve("start"); }); } start() .then(data => { // promise start console.log("result of start: ", data); return Promise.resolve(1); // p1 }) .then(data => { // promise p1 console.log("result of p1: ", data); return Promise.reject(2); // p2 }) .then(data => { // promise p2 console.log("result of p2: ", data); return Promise.resolve(3); // p3 }) .catch(ex => { // promise p3 console.log("ex: ", ex); return Promise.resolve(4); // p4 }) .then(data => { // promise p4 console.log("result of p4: ", data); });
上面的代碼最終會(huì)輸出:
result of start: start result of p1: 1 ex: 2 result of p4: 4
代碼的執(zhí)行邏輯如圖:
從圖中可以看出來(lái),代碼的執(zhí)行邏輯是 promise start --> promise p1 --> promise p3 --> promise p4。所以結(jié)合輸出結(jié)果和執(zhí)行邏輯圖,總結(jié)出以下幾點(diǎn):
promise 的 then 方法里面可以繼續(xù)返回一個(gè)新的 promise 對(duì)象
下一個(gè) then 方法的參數(shù)是上一個(gè) promise 對(duì)象的 resolve 參數(shù)
catch 方法的參數(shù)是其之前某個(gè) promise 對(duì)象的 rejecte 參數(shù)
一旦某個(gè) then 方法里面的 promise 狀態(tài)改變?yōu)榱?rejected,則promise 方法連會(huì)跳過(guò)后面的 then 直接執(zhí)行 catch
catch 方法里面依舊可以返回一個(gè)新的 promise 對(duì)象
How to Break Promise Chain接下來(lái)就該討論如何中止 promise 方法鏈了。
通過(guò)上面的例子,我們可以知道 promise 的狀態(tài)改變?yōu)?rejected 后,promise 就會(huì)跳過(guò)后面的 then 方法。
也就是,某個(gè) then 里面發(fā)生異常后,就會(huì)跳過(guò) then 方法,直接執(zhí)行 catch。
所以,當(dāng)在構(gòu)造的 promise 方法鏈中,如果在某個(gè) then 后面,不需要再執(zhí)行 then 方法了,就可以把它當(dāng)作一個(gè)異常來(lái)處理,返回一個(gè)異常信息給 catch,其參數(shù)可自定義,比如該異常的參數(shù)信息為 { notRealPromiseException: true},然后在 catch 里面判斷一下 notRealPromiseException 是否為 true,如果為 true,就說(shuō)明不是程序出現(xiàn)異常,而是在正常邏輯里面中止 then 方法的執(zhí)行。
代碼大概就這樣:
start() .then(data => { // promise start console.log("result of start: ", data); return Promise.resolve(1); // p1 ) .then(data => { // promise p1 console.log("result of p1: ", data); return Promise.reject({ notRealPromiseException: true, }); // p2 }) .then(data => { // promise p2 console.log("result of p2: ", data); return Promise.resolve(3); // p3 }) .catch(ex => { console.log("ex: ", ex); if (ex.notRealPromiseException) { // 一切正常,只是通過(guò) catch 方法來(lái)中止 promise chain // 也就是中止 promise p2 的執(zhí)行 return true; } // 真正發(fā)生異常 return false; });
這樣的做法可能不符合 catch 的語(yǔ)義。不過(guò)從某種意義上來(lái)說(shuō),promise 方法鏈沒(méi)有繼續(xù)執(zhí)行,也可以算是一種“異常”。
Refactor Callback with Promise講了那么多道理,現(xiàn)在就改來(lái)使用 promise 重構(gòu)之前用回調(diào)函數(shù)寫(xiě)的異步邏輯了。
// 據(jù) name 查詢(xún)用戶(hù)信息 const findUserByName = (name, pwd) => { return new Promise((resolve, reject) => { // 數(shù)據(jù)庫(kù)查詢(xún)操作 if (dbError) { // 數(shù)據(jù)庫(kù)查詢(xún)出錯(cuò),將 promise 設(shè)置為 rejected reject({ code: 1000, message: "查詢(xún)用戶(hù)信息,數(shù)據(jù)庫(kù)操作數(shù)出現(xiàn)異常", }); } // 將查詢(xún)結(jié)果賦給 userinfo 變量 if (userinfo.length === 0) { // 數(shù)據(jù)庫(kù)中不存在該用戶(hù) resolve(); } // 數(shù)據(jù)庫(kù)存在該用戶(hù),判斷密碼是否正確 if (pwd === userinfo[0].pwd) { // 密碼正確,中止 promise 執(zhí)行 reject({ notRealPromiseException: true, data: { code: 0, message: "密碼正確,登錄成功", } }); } // 密碼不正確,登錄失敗,將 Promise 設(shè)置為 Rejected 狀態(tài) reject({ code: 1001, message: "密碼不正確,登錄失敗", }); }); }; // 模擬登錄教務(wù)系統(tǒng) const loginEducationSystem = (name, pwd) => { // 登錄邏輯... // 登錄成功 resolve(); // 登錄失敗 reject({ code: 1002, message: "模擬登錄教務(wù)系統(tǒng)失敗", }); }; // 將用戶(hù)名密碼存入數(shù)據(jù)庫(kù) const saveUserToDB(name, pwd) => { // 數(shù)據(jù)庫(kù)存儲(chǔ)操作 if (dbError) { // 數(shù)據(jù)庫(kù)存儲(chǔ)出錯(cuò),將 promise 設(shè)置為 rejected reject({ code: 1004, message: "數(shù)據(jù)庫(kù)存儲(chǔ)出錯(cuò),將出現(xiàn)異常", }); } // 數(shù)據(jù)庫(kù)存儲(chǔ)操作成功 resolve(); }; findUserByName(name) .then(() => { return loginEducationSystem(name, pwd); }) .then(() => { return saveUserToDB(name, pwd); }) .catch(e => { // 判斷異常出現(xiàn)原因 if (e.notRealPromiseException) { // 正常中止 promise 而故意設(shè)置的異常 return res.json(e.data); } // 出現(xiàn)錯(cuò)誤或異常 return res.json(e); });
在上面的代碼中,實(shí)例化了三個(gè) promise 對(duì)象,分別實(shí)現(xiàn)業(yè)務(wù)需求中的三個(gè)功能。然后通過(guò) promise 方法鏈來(lái)調(diào)用。相比用回調(diào)函數(shù)而言,代碼結(jié)構(gòu)更加清晰,也更易讀易懂耦合度更低更易擴(kuò)展了。
Promise.all && Promise.race仔細(xì)觀察可以發(fā)現(xiàn),在上面的 promise 代碼中,loginEducationSystem 和 saveUserToDB 兩個(gè)方法執(zhí)行有先后順序要求,但沒(méi)有數(shù)據(jù)傳遞。
其實(shí) promise 方法鏈更好用的一點(diǎn)是,當(dāng)下一個(gè)操作依賴(lài)于上一個(gè)操作的結(jié)果的時(shí)候,可以很方便地通過(guò) then 方法的參數(shù)來(lái)傳遞數(shù)據(jù)。前面頁(yè)提到過(guò),下一個(gè) then 方法的參數(shù)就是上一個(gè) then 方法里面 resolve 的參數(shù),所以當(dāng)然就可以把上一個(gè) then 方法的執(zhí)行結(jié)果作為參數(shù)傳遞給下一個(gè) then 方法了。
還有些時(shí)候,可能 then 方法的執(zhí)行順序也沒(méi)有太多要求,只需要 promise 方法鏈中的兩個(gè)或多個(gè) promise 全部都執(zhí)行正確。這時(shí),如果依舊一個(gè)一個(gè)去寫(xiě) then 可能就比較麻煩,比如:
function p1() { return new Promise((resolve) => { console.log(1); resolve(); }); } function p2() { return new Promise((resolve) => { console.log(2); resolve(); }); } function p3() { return new Promise((resolve) => { console.log(3); resolve(); }); }
現(xiàn)在只需要 p1 p2 p3 這三個(gè) promise 都執(zhí)行,并且 promise 最終狀態(tài)都是 Fulfilled,那么如果還是使用方法鏈,這是這樣調(diào)用:
p1() .then(() => { return p2(); }) .then(() => { return p3(); }) .then(() => { console.log("all done"); }) .catch(e => { console.log("e: ", e); }); // 輸出結(jié)果: // 1 // 2 // 3 // all done
代碼貌似就不那么精煉了。這個(gè)時(shí)候就有了 Promise.all 這個(gè)方法。
Promise.all 接收一個(gè) promise對(duì)象的數(shù)組作為參數(shù),當(dāng)這個(gè)數(shù)組里的所有 promise 對(duì)象全部變?yōu)?resolve 或 reject 狀態(tài)的時(shí)候,它才會(huì)去調(diào)用 then 方法。
于是,調(diào)用這幾個(gè) promise 的代碼就可以這樣寫(xiě)了:
p1() .then(() => { return Promise.all([ p2(), p3(), ]); }) .then(() => { console.log("all done"); }) .catch((e) => { console.log("e: ", e); }); // 輸出結(jié)果: // 1 // 2 // 3 // all done
這樣看起來(lái)貌似就精煉些了。
而對(duì)于 Promise.race,其參數(shù)也跟 Promise.all 一樣是一個(gè)數(shù)組。只是數(shù)組中的任何一個(gè) promise 對(duì)象如果變?yōu)?resolve 或者reject 的話,該函數(shù)就會(huì)返回,并使用這個(gè) promise 對(duì)象的值進(jìn)行 resolve 或者 reject。
這里就不舉例了。
Conclusion到目前為止,我們就基本了解了 Promise 的用法及特點(diǎn),并實(shí)現(xiàn)用 Promise 重構(gòu)用回調(diào)函數(shù)寫(xiě)的異步操作。現(xiàn)在對(duì) Promise 的使用,應(yīng)該駕輕就熟了。
完。
Github Issue: https://github.com/nodejh/nodejh.github.io/issues/23
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/81048.html
摘要:使用對(duì)象的好處在于可以將異步操作以同步操作的流程表達(dá)出來(lái),避免了層層嵌套的回調(diào)函數(shù)。對(duì)象異步操作拋出錯(cuò)誤,狀態(tài)就會(huì)變?yōu)椋蜁?huì)調(diào)用方法指定的回調(diào)函數(shù)處理這個(gè)錯(cuò)誤。 Promise 含義 Promise 是異步編程的一種解決方案,比傳統(tǒng)的解決方案——回調(diào)函數(shù)和事件——更合理和更強(qiáng)大。它由社區(qū)最早提出和實(shí)現(xiàn),ES6 將其寫(xiě)進(jìn)了語(yǔ)言標(biāo)準(zhǔn),統(tǒng)一了用法,原生提供了 Promise 對(duì)象。 所謂 P...
摘要:的和我們通過(guò)的原型方法拿到我們的返回值輸出我延遲了毫秒后輸出的輸出下列的值我延遲了毫秒后輸出的。有人說(shuō),我不想耦合性這么高,想先執(zhí)行函數(shù)再執(zhí)行,但不想用上面那種寫(xiě)法,可以嗎,答案是當(dāng)然可以。 此文只介紹Async/Await與Promise基礎(chǔ)知識(shí)與實(shí)際用到注意的問(wèn)題,將通過(guò)很多代碼實(shí)例進(jìn)行說(shuō)明,兩個(gè)實(shí)例代碼是setDelay和setDelaySecond。 tips:本文系原創(chuàng)轉(zhuǎn)自...
摘要:綜上,對(duì)進(jìn)行一定的封裝,來(lái)簡(jiǎn)化編碼操作。化的嘗試對(duì)于這種帶大量回調(diào)的,使用進(jìn)行異步化封裝是個(gè)好主意。因此包括在內(nèi)的所有異步方法都會(huì)強(qiáng)制中止當(dāng)前事務(wù)。這就決定了一個(gè)事務(wù)內(nèi)部的所有操作必須是同步完成的。目前只實(shí)現(xiàn)了和,其他的有待下一步工作。 前言 本文是介紹我在編寫(xiě)indexedDB封裝庫(kù)中誕生的一個(gè)副產(chǎn)品——如何讓indexedDB在支持鏈?zhǔn)秸{(diào)用的同時(shí),保持對(duì)事務(wù)的支持。項(xiàng)目地址:htt...
摘要:參數(shù)如前面所提到的,方法只是方法的一個(gè)語(yǔ)法糖,原因就在于方法的參數(shù)為實(shí)際上是兩個(gè)回調(diào)函數(shù),分別用于處理調(diào)用它的對(duì)象的和狀態(tài),而方法就等價(jià)于狀態(tài)處理函數(shù)。對(duì)象狀態(tài)傳遞和改變的方法利用回調(diào)的返回值,可以控制某個(gè)操作后方法返回的對(duì)象及其狀態(tài)。 注意,本文主要針對(duì)ES6標(biāo)準(zhǔn)實(shí)現(xiàn)的Promise語(yǔ)法進(jìn)行闡述,實(shí)例代碼也都使用ES6語(yǔ)法,快速入門(mén)ES6請(qǐng)參見(jiàn)ECMAScript 6 掃盲。 一分鐘...
摘要:前言使用中,鏈?zhǔn)降恼{(diào)用對(duì)于控制異步執(zhí)行很重要。的鏈?zhǔn)秸{(diào)用是支持鏈?zhǔn)秸{(diào)用的,但是它是不同于上面的鏈?zhǔn)健J钦{(diào)用方法返回自身,但是是調(diào)用方法后返回一個(gè)新的。的運(yùn)行機(jī)制請(qǐng)參考的運(yùn)行機(jī)制值穿透由于通過(guò)沒(méi)有成功添加回調(diào)函數(shù),發(fā)生了值穿透。 前言 使用Promise中,鏈?zhǔn)降恼{(diào)用對(duì)于控制異步執(zhí)行很重要。 鏈?zhǔn)秸{(diào)用 在jQuery的使用中,我們常常使用下面的代碼 $(#app).show().css(...
閱讀 2825·2021-10-08 10:04
閱讀 3285·2021-09-10 11:20
閱讀 536·2019-08-30 10:54
閱讀 3331·2019-08-29 17:25
閱讀 2314·2019-08-29 16:24
閱讀 896·2019-08-29 12:26
閱讀 1455·2019-08-23 18:35
閱讀 1946·2019-08-23 17:53