淺談 JavaScript 的 Promise
- 分類:
- 字數: x 82雞數:計算文長的常見計量單位,一般而言數字大小與文章長度呈正相關
本文會著重介紹 ES2015 定義的原生 Promise,除了基本的用法和個人使用心得外,還會補充一些原生 Promise 不足處的可能解決方案。適合完全沒用過 Promise 的生手閱讀,以上。
總之,讓我們開始吧!
***
實用工具誕生的目的,就是為了解決我們生活中碰到的種種問題。
身為宅男工程師,我們碰到最大的問題就是--沒女朋友。
當然不是說 Promise 可以充當女友,畢竟也沒悲情到這種地步……吧?也……也許?嗯嗯……但善用 Promise 可以解決許多我們在「追女友」遇到的麻煩。
怎麼說呢?事情是這樣子的……
妹子的矜持 - 事情不是同步的
首先,理想中追女友的情況:
// 告白服務由「心儀妹子」獨家提供
const yes = 告白服務.告白(心儀妹子, 我)
if (yes) {
// 耶~去看電影!
} else {
// 哭哭,去酒吧買醉 T^T
}
但事情遠非如此簡單。
現實中,妹子並不會立即回覆「告白」,她們需要時間醞釀、考慮、權衡利弊,經過閨密投票等多道複雜手續,才能給予最後審核結果。
即使妹子心中早已有答案,流程也是必要的。縱然心中千想萬想,仍得故作矜持,給予懸念。如此你在得到她後,能更加珍惜;反之,她完全看不上你,同樣也得等一段時間,表示她「真的」有在考慮……
是以現實妹子實際提供的「介面」更像是這般:
// 心儀妹子透過「回覆函式」事後回傳結果。
告白服務.告白(心儀妹子, 我, function 回覆函式 (yes) {
if (yes) {
// 耶~去看電影!
} else {
// 哭哭,去酒吧買醉 T^T
}
})
「告白」函式執行後,不會立即回傳結果,而是透過「回覆函式」回傳。此種開發風格稱作 CPS (Continuation-passing style)。在 JavaScript 的世界,常用這種風格解決異步操作的問題。
如此一來,心儀妹子不必馬上回應,能有時間更從容應對,並在想清楚答覆後,利用「回覆函式」回傳--最終審判結果。
但是--只要和妹子扯上關係,事情就絕對不會簡單!
妹子的複雜性 - CPS 風格不好寫
並非 CPS 不好,但要寫得好不容易。
難懂的非線性思維 - 現在和未來的程式碼寫在一起
「我喜歡妳!」
「如果結婚的話,我希望還能有自己的空間……」
「什麼?」
「有了空間後,我想要養小雞!」
「為什麼要養小雞!?等等……結婚!所以妳是答應了嗎?」
「不,那是未來結婚的話,但我們現在還沒交往。」
「……那妳願意和我交往嗎?」
「不願意。」
……
我們不太擅長異步的思考,偏偏 CPS 風格常迫使我們用跳躍式的寫法。
// 時間點 1
告白服務.告白(心儀妹子, 我, function 回覆函式 (女友) {
// 時間點 3
結婚服務.結婚(女友, 我, function 回覆函式 (老婆) {
// 時間點 5
養雞服務.養雞 (20, function 回覆函式 (雞群) {
// 時間點 7
})
// 時間點 6
})
// 時間點 4
})
// 時間點 2
長久以來,我們習慣程式碼由上而下依序執行,但這種寫法的程式碼卻可能先執行頭尾,最後才執行中間。不同執行時間的程式碼夾雜在一起,不容易讓讀者理解。
困惑的百千種風格 - 操作方式不一致
即使無邊無際的花海,也沒有一朵花完全相同。妹子各有不同的喜好,其「告白介面」自然也不會一樣。同是追女友,卻得使用各種不同方式追,增添許多人的困擾。
好比說指定「回覆函式」:
// 可能以「有沒有成功?」的方式回覆
告白服務1.告白(心儀妹子, 我, function 回覆函式 (yes) {
if (yes) {
// 成功脫單
} else {
// 脫單失敗
}
})
// 可能以「有沒有失敗?」的方式回覆 (如果沒失敗就是成功)
告白服務2.告白(心儀妹子, 我, function 回覆函式 (no) {
if (!no) {
// 成功脫單
} else {
// 脫單失敗
}
})
// 可能不直接以回覆函式回傳結果
告白服務3.告白(心儀妹子, 我, function 回覆函式 () {
if (告白服務3.result === 'yes') {
// 成功脫單
} else {
// 脫單失敗
}
})
// 可能回傳給不同函式
告白服務4.告白(心儀妹子, 我)
告白服務4.on('yes', function 成功函式 () {
// 成功脫單
})
告白服務4.on('no', function 失敗函式 () {
// 脫單失敗
})
由於不必即時回覆,因此「回覆函式」指定的順序、方式都沒差別,連「回覆函式」本身的行為也能有不同的變化,加總起來足以讓人眼花撩亂。
但其實「告白方式」和「告白內容」應該是可以區別的,重要的應該是「內容」而非「方式」才對。多餘的「方式」只是增添彼此的障礙罷了。
陰險的不可預測性 - 回調不代表異步
妹子通常都是第三方函式庫,因此不保證行為如你預期,或著該說--保證不如你預期!(笑)
由於無法管控妹子如何實作「告白」,所以各種可能都會發生。假使她偏用同步的方式使用回調,你也拿她沒辦法。
//現在發生的事
告白服務.告白(心儀妹子, 我, function 回覆函式 () {
// 可能是未來發生,也可能是立刻就發生,端看實作告白的人而定
})
// 還是現在發生的事
妹子一句「你真討厭~」,可能代表「喜歡你」,也可能代表「真的討厭你」,表面完全相同的形式卻可能有完全不同的意涵,這--就是妹子的不可預測性!
眼花的多段式操作 - 回調地獄
總之,我們可以先不用著急最後答案,畢竟妹子需要時間考慮,必須等待異步的結果。
但我們仍可先想像「如果有女友的話,接下來該怎麼做?」畢竟希望自己有女友,必定是有很多「如果有女友,我想做……」的事情。 (害羞)
比如說--結婚!俗話說得好--不以結婚為目標的交往都是詐欺!
(誰說的啊!?)
雖然並非每對情侶都能走到這一步,但相信大家都是以此為目標一同前行的!
結婚前要先求婚,求婚前總要先交往。我們不會搞錯順序,先結婚再求婚,也不會先求婚再交往。而且是前者確實完成後,才會有後續,不能同時進行。畢竟總不能和對方說:「我想和妳交往、求婚和結婚,妳願意同時和我交往、求婚和結婚嗎?」
告白服務.告白(心儀妹子, 我, function 回覆函式 (女友) {
if (女友) {
求婚服務.求婚(我, 女友, function (未婚妻) {
if (未婚妻) {
結婚服務.結婚(我, 未婚妻, function (老婆) {
if (老婆) {
// ...
}
})
}
})
}
})
當事情必須一個接著一個執行,後面的回調會用到前面回調的結果時,當層數一多,便能見到所謂的「回調地獄 (callback hell)」。這種結構很難平行展開,時常會越疊越深,因此很容易變得複雜而難懂。
此外,事情也可能會失敗,假使對方嫌你求婚時不夠浪漫,可能就得再來一次。所以這裡用的 if 還可能再接別的 else,else 再接別的回調函式……
配合前述的幾個麻煩混合在一起,「回調地獄」可不是浪得虛名的,甚至還有網站「 Callback Hell 」專門在說明追妹子的種種辛苦……種種「地獄」的感受……
如果此時有英勇人士,能跳出來解決廣大宅男們的困境,應該會被奉為救世主吧?
然後……還真有人跳出來了,他揮灑著熱汗,揮著手,堅定地從遙遠的地平線跑過來。
遠遠的他好像在吼叫?他究竟想說什麼呢?
閉上眼,仔細聆聽,字句似乎越來越清晰?
「看標題!看標題!看標題!看標題!……」
看什麼標題?
「看文章標題!看文章標題!看文章標題!」
什麼文章標題?
突然一個小男孩猛然頓悟,狠狠抱住身旁的小女孩,雙手亂摸亂抓,並大聲喊道:「Promise!Promise!Promise!……」
這時圍觀的群眾們福至心靈,也一同吼道:「Promise!Promise!Promise!……」
沒錯,拯救宅男的救世主-- Promise 出現了!
(然後小男孩就被小女孩痛扁了。)
公用的「告白平台」 - Promise
為何要用公用告白平台? - 何謂 Promise?
嚴格來講,上面提的很多問題不見得真的是「問題」,至少並非是「事情做不到」,而更像是「怎麼做都行,所以不知道怎麼做」。大家皆可用自己的方式使用回調,反而造成使用上的總總困擾。
而 Promise 其實就是一個第三方公正平台,除了提供一些更方便解決問題的方法外,更要緊的是提供了規範化的流程,讓大家能用統一的方式處理異步。
有了「告白平台」,就能減少和不同妹子告白的成本。因為這個「平台」不是個別妹子做的,你很清楚不會發生不同妹子不同行為的情況。只要妹子有提供 Promise 的告白介面,那你就可以預期對方的行為,減少誤解、或被誤解的可能。
你知道告白成功會發生什麼,即便失敗也能明確得到答案。
這邊先簡單介紹 Promise 的來由。
從前從前,有一個稱作 CommonJS 的組織先後提出了 Promises/A、Promises/B、Promises/KISS、Promises/C、Promises/D 等方案。其中我們只需要知道 Promises/A 就行了,這個方案主要定義了像是 Promise 物件為何,或是 then 大致的行為等。
後來又有人根據 Promises/A 制定了 Promises/A+,方案除了一些細微的更動外,主要是更加詳細定義了 then 的行為細節。估計感覺不錯,所以有很多根據 Promises/A+ 標準的實作品紛紛冒出,像是 Q、Bluebird 等。
然後……實在是很好用,也很多人在用,所以最後 Promise 被列進 ES2015 裡,修成正果,得道升天。
目前除了 IE11 外,幾乎所有的瀏覽器已經內建支援 Promise 了。即使沒有,也可用第三方函式庫,原則上已經不需要擔心支援度的問題。
那這種方式到底是什麼呢?又好在那裡呢?
讓我們先來看看使用 Promise 的例子:
告白服務.告白(心儀妹子, 你)
.then(function 成功函式 (女友) {
})
.catch(function 失敗函式 (失敗原因) {
})
與之前回調函式的版本差異不大。成功時,會執行 then 傳入的「成功函式」;異常時,會執行 catch 的「失敗函式」。
但 Promise 的版本保證「妹子一定是以異步的方式」給予回覆。
假使一位高富帥,腦袋聰明又努力,對待妹子浪漫又貼心,但即便被如此完美的白馬王子告白,妹子也沒辦法立刻搶著表示:「我答應!我答應!」
這樣一來,至少你和高富帥都能一致的「異步」得到結果,而不是對你「同步」馬上拒絕……至少就不會發生前面提的「不可預測性」的問題了。
此外,如果碰到前述需要一項接著一項做的事情,then 本身也可以再接別的 then,前一個 then 的回調函式執行完,就會執行接續下一個 then 的回調函式,前者的回傳值會當成後者的參數傳入,用先前的例子就會變成這樣:
告白服務.告白 (心儀妹子, 你)
.then(求婚服務.求婚)
.then(結婚服務.結婚)
.catch(function 失敗函式 (失敗原因) {
})
看來多可愛?Promise 並不是要幹掉回調函式,而是能讓原先散落各處的回調函式,以非常一致且直觀的方式組織起來。你能清晰看出各個回調函式是如何串接執行的,不用再看到噁心的巢狀回調,而且處理失敗情況時也明確簡單許多。
至於「告白」裡頭是怎麼實作?為什麼後面可以接 then 函式呢?
原因在於「告白」函式裡頭回傳了一個「Promise 物件」。
class 告白服務 {
// ...
告白 = (本妹子, 目標男) => {
return new Promise (function 審核函式 (resolve, reject) {
if (是否審核通過(目標男)) {
resolve(new 女友(本妹子)) // 代表答應
} else {
reject(new Error('你是個好人,但我一直都當你是哥哥……')) // 裡頭寫失敗的理由。
}
})
}
}
建立 Promise 物件時,會傳入一個 executor 函式(此例為「審核函式」),函式有兩個參數,分別是「resolve」和「reject」兩個函式。
此例中,若最後妹子選擇「答應」,就會在 exector 裡頭執行「resolve」函式。執行時可以傳值進去代表執行的結果,此例來說自然就是心愛的「女友」啦!反過來說,如果選擇「不答應」,那麼她就會執行「reject」函式,並傳入失敗的原因,習慣上「原因」會以 Error 物件的形式回傳。
要注意 executor 函式本身會在建立 Promise 物件時立刻執行,但是結果(此例是「女友」)卻會是以異步的方式回傳(前面有強調過)。
由於是異步,「告白」函式回傳的 Promise 物件,在當下可能有值也可能沒值。所以裡頭實際上存在著三種可能的狀態,分別是「fulfilled」、「reject」和「pending」。顧名思義,其分別對應著「實現」、「拒絕」和「擱置」或是更直白的說法即是「成功」、「失敗」和「等待中」。
Promise 的狀態一開始會是「pending」,完成就會變成「fulfilled」。反之則變成「reject」,你可以查詢失敗的理由,想想為什麼會有這樣的結果和自己悲哀的人生……
但至少不論是成功還是失敗,之後這個 Promise 物件的狀態都不會再改變,不可能有先是 fulfilled ,隔一陣子突然變成 reject 狀況,反之亦然。因此不會有「先答應你,事後再告你強暴……」的鳥事發生。
說了這麼多,其實大部分的情況我們不太需要在意妹子具體怎麼實作「告白」的,畢竟通常我們就只是使用者,我們更在乎後頭 then 怎麼組織,裡頭的「成功函式」怎麼實作,不需要考慮 Promise 物件本身怎麼來的,只要用即可。
需要在意的是妹子,因為她們是服務提供者,她們必須知道如何將自己的「告白服務」轉為 Promise 的型式。畢竟對她們而言,來源是多多益善,這樣比較容易找到理想的如意郎君,而不只有死宅男可以選。
Note
但事情也不是這麼絕對,因為「服務提供者」也不見得非要提供這種方式不可。所以有時候我們也需要自己將第三方服務包成 Promise 的形式,這時可以選擇某些第三方 Promise 庫提供的工具,方便我們將函式包成 Promise 的版本。
如果告白成功呢? - then
Promise 物件提供了成員函式 then,then 可以傳 onFulfilled 回調函式進去,若該 Promise 狀態變為 fulfilled,就會執行此回調函式。
前例的告白函式成功執行時,會回傳一個「女友」(利用 resolve 傳遞,可以是任何東西,只是此例是「女友」),接著「女友」就會被當成參數傳入 onFulfilled 函式。
告白服務.告白 (心儀妹子, 我)
.then(牽手) // 「女友」會被當成「牽手」回調函式的參數。
執行 then 函式後本身會回傳另一個新的 Promise 物件,當 then 的回調函式執行成功時,這個新 Promise 物件的狀態就會變為「成功」,反之如果回調函式在執行過程中擲出異常,就會變為「失敗」。
也因為 then 回傳的也是 Promise 物件,所以才能用 then 串接下去。此例來說,有女友後自然不會只是牽手,肯定還會有下一步,畢竟男人的欲望可沒這麼容易滿足的!那寫起來就會樣:
const promise1 = 告白服務.告白 (心儀妹子, 我)
const promise2 = promise1.then(牽手)
const promise3 = promise2.then(擁抱)
const promise4 = promise3.then(接吻)
// 或合在一起
告白服務.告白 (心儀妹子, 我)
.then(牽手)
.then(擁抱)
.then(接吻)
假設「告白」本身的結果是「成功」,那對應的 promise1 物件狀態就會變成「成功」,並執行「牽手」函式。如果接下來「牽手」也執行成功,就會換成 promise2 物件變成「成功」,並執行「擁抱」,以此類推。
告白服務.告白 (心儀妹子, 我)
.then(function 牽手(女友) {
// 「女友」是「告白」的回傳值
// 以某種方法和女友牽手 (羞)
return 女友 // 直接回傳,當成「擁抱」的參數
})
.then(擁抱)
.then(接吻)
如果告白失敗呢? - catch
有成功自然就有失敗,失敗也需要有對應的計劃,畢竟人生還是要過嘛!因此 catch 就可以出場了。與 then 相反,catch 可以傳入 onRejected 函式,顧名思義,會在上一個 Promise 失敗時執行。
同樣的,catch 函式本身也會回傳新的 Promise 物件,因此也可以像 then 一樣不斷串接下去。
舉個例子:
告白服務.告白 (心儀妹子, 我)
.catch(function 失敗的補救方案 (error) {
// error 是「告白」回傳的 Promise 物件執行 reject 的回傳值
console.log(error.message) // 我只把你當哥哥
return 右手
})
.then(牽手) // 只要「失敗的補救方案」,一樣會執行接下來的「牽手」
.then(擁抱)
.then(接吻)
假設「告白」失敗,那傳給 catch 的「失敗的補救方案」函式就會被執行。此處很容易誤解的地方是--雖然「告白」失敗了,但只要失敗時有正常執行「失敗的補救方案」,那麼對應的 Promise 本身就算是成功的!
換言之,如要這裡的「失敗的補救方案」有正常回傳「右手」,之後還是會正常執行「牽手」函式,只不過傳入的參數自然就是你的「右手」囉。 (右手和自己牽手?)
身為一個專業的宅宅,都應該要培養一個好習慣--不管結果是什麼,即是再哀傷,最後都會用 catch 函式 接住所有可能的錯誤!這樣才是負責任的好宅宅!
// ...
.catch(function 善後 () {
// 做為有負責任的宅宅該做的事
})
說再深入一些,catch 函式其實只是 then 的語法糖,事實上 then 也可以將 onRejected 當作第二個參數,代表「如果失敗時會執行的回調函式」,其實也就是 catch 在做的事。
// 前例可以改寫成
告白服務.告白 (心儀妹子, 我)
.then (undefined, function 失敗的補救方案 (error) { // 完全等同於 catch
// ...
})
雖然通常我們只要用 catch 即可,但在 IE8 以下的瀏覽器中,由於 catch 和 try ... catch 的 catch 同名,會產生名稱方面的衝突,因此只能使用 then,不然會爆炸。但山不轉路轉,舉例來說,有的第三方 Promise 函式庫會改用 caught 這個名稱取代 catch。
你可能會好奇,傳給 Promise 的 executor 函式可以用 reject 回傳失敗的結果,那傳給 then 的 onFulfilled 函式或 catch 的 onRejected 函式執行失敗的話又該怎麼做呢?
其實無論是那一種都有一個很簡單的方式代表 Promise 執行失敗,那就是簡單的執出異常即可。
executor 函式的例子:
class 告白服務 {
// ...
告白 = (本妹子, 目標男) => {
return new Promise (function 審核函式 (resolve, reject) {
if (目標男 === '兩大類') {
throw new Error('滾!廢物!') // 可以直接擲出異常
}
///...
})
}
}
告白服務.告白 (心儀妹子, '兩大類')
.catch(error) {
// 可以用 catch 接到「告白」函式擲出的 error
}
onFulfilled 函式的例子:
告白服務.告白 (心儀妹子, 我)
.then(function 牽手(女友) {
throw new Error('雖然人家願意當女友,但是不給牽!')
})
.catch(error) {
// 一樣可以用接到「牽手」函式擲出的 error
}
通常兩種方法沒有任何差別,但在有些情況只有 reject 可以正常運作。
class 告白服務 {
// ...
告白 = (本妹子, 目標男) => {
return new Promise (function 審核函式 (resolve, reject) {
setTimeout(function() {
throw new Error('滾!廢物!') // 這裡如果 throw 沒有效果,只能用 reject
}, 1000)
///...
})
}
}
不過正如先前所提,多數情況我們都只是「服務」的使用者,只會使用到 then 或 catch,所以前面的東西不用太在意,用熟就會了。只要知道實作 onFulfilled 或 onRejected 函式時,成功直接回傳,而失敗時就讓它執出異常即可。
Note
另外,其實還可以用後頭提的 Promise.reject 來做到 reject 的效果。
告白服務.告白 (心儀妹子, 我)
.then(function 牽手(女友) {
return Promise.reject(new Error('雖然人家願意當女友,但是不給牽!'))
})
.catch(error) {
// 一樣可以用接到「牽手」函式擲出的 error
}
接下來,讓我們深呼吸幾次,回過頭來整理一下發生了什麼,我們知道了為什麼要有 Promise?知道了什麼是 Promise 物件以及要怎麼使用?另外還了解了 then 和 catch 的使用方式。
所以 Promise 就這樣了嗎?
當然不是!
那如果不知道成功還是失敗呢? - 回傳值
扯到交往,你以為事情會這麼簡單嗎?
當然不會,人家可是妹子耶!
你以為告白後,妹子就會立刻給予答覆嗎?
當然不可能,人家可是妹子耶!
你可能得等一段時間,異步等待妹子給予答覆。
等到妹子回覆了,你以為這樣就有結束了嗎?
當然不一定,人家可是妹子耶!
可……可是不是成功就是失敗,難道還能有別的結果?
當然有,那就是--人家自己也還不知道!
啥?
其實妹子在實作告白時,最後不見得會直接回傳一個「女友」,而可以是另一個 Promise 物件。
何解?
因為對方可能早已有男友啦!所以她可能會說:「我得先和現任男友分手才能和你交往。」
換言之,她必須要和男友解約成功了才能有後續,解約失敗當然就一切免談了。而和男友分手也需要時間,所以回傳另一個 Promise 物件給你。
class 告白服務 {
// ...
告白 = (本妹子, 目標男) => {
return new Promise (function 審核函式 (resolve, reject) {
if (是否審核通過(目標男)) {
// 回傳另一個 Promise
resolve(new Promise (function 和男友解約(resolve, reject) {
if (是否解約成功()) {
resolve(new 女友(本妹子)) // 與男友解約成功的話就回傳女友
}
}))
}
})
}
}
解約當然也可能失敗,想當然爾,不管中間過程如何,這個 Promise 自然也就「失敗」了。
說是這樣說,但其實使用者不需要管上一個 Promise 回傳的究竟是值還是 Promise。因為如果回傳的是 Promise 物件,那 Promise 會自動等到該 Promise 物件的結果出來後,才回傳最終結果。
告白服務.告白 (心儀妹子, 我)
.then(牽手) // 成功時「牽手」收到的仍是「女友」而非 Promise 物件
.then(擁抱)
.then(接吻)
此例來說,即使「告白」的結果回傳的是另一個 Promise 物件,後面「牽手」函式其實接到的參數仍會是「女友」,而非中間過程的 Promise 物件。
即使 Promise 物件的結果又是另一個新的 Promise 物件,它也會繼續找,不斷重覆下去,直到不是 Promise 的結果出現為止。
也就是說 Promise 也可以寫得像回調地獄一樣,舉一個比較實際的例子:
// fetch 會抓取目標網址的內容,並且回傳 Promise
fetch('https://api.marco79423.net/api/articles/')
.then(res => {
return res.json() // 拿到回傳值後,可以轉成 json 格式,一樣會回傳 Promise
.then(articles => {
console.log(articles) // 成功拿到內容
})
})
但 Promise 的好處是可以平展開:
// 也可以直接展開
fetch('https://api.marco79423.net/api/articles/')
.then(res => res.json())
.then(articles => {
console.log(articles)
})
而 CPS 的方式就不能如此,但也並非全展開就是好,因為有時適當的調整深度可以做到傳遞參數的效果,比如說剛剛例子中:
告白服務.告白 (心儀妹子, 我)
.then(牽手) // 牽手必須將「告白」傳入的「女友」傳下去
.then(擁抱)
.then(接吻)
若 then 要可接下去,「牽手」、「擁抱」、「接吻」都必須將「女友」當成結果傳下去,不然後面的就沒有「女友」可以抱了。
但如果這時改成:
告白服務.告白 (心儀妹子, 我)
.then(function (女友) {
return 牽手() // 可能要包成回傳 Promise 的版本
.then(擁抱) // 可以直接拿到外面的「女友」
.then(接吻)
})
// 或是用後頭會提的 Promise.resolve
告白服務.告白 (心儀妹子, 我)
.then(function (女友) {
return Promise.resolve()
.then(牽手)
.then(擁抱)
.then(接吻)
})
這樣「牽手」、「擁抱」和「接吻」就可以直接拿到上一層作用域的「女友」而不需要透過參數傳入了。
整理一下,所以傳入 resolve 和 reject 函式的值和 then/catch 的回傳值,總共可以有三種類型,分別是:
類型 | 解釋 |
---|---|
Value | 一般值 |
Promise | Promise 物件 |
Thenable | 有實作 then 的物件 |
Value 就是所謂的一般值,剛剛的例子就是「女友」,Promise 即是剛才提到的 Promise 物件。
至於最後的 Thenable 則是為了相容之前的函式庫而定的。考慮到 Promise 是後來者,很多「準情侶」還不知道有這個好東西,但上天本身就會自己找一個出路。即使不是 Promise,但要是看起來很像 Promise,那就先假裝當 Promise 吧!雖然不是真貨,但假使人家明明都已經寫好黑紙白字「本姑娘在此鄭重宣示要當 XXX 的女友」,那當然就先相信嘍,不然要拒絕嗎?
所以 Thenable 到底是什麼呢?就是只要有實作 then 函式的物件就是 Thenable,在此可以直接充當 Promise 的方式使用。
宅男告白利器 - Promise 常用工具
平行告白工具 - Promise.race
身為一位有自覺的宅男,你不用期待自己的告白一定能成功,為了增加命中率,實務上可能會採取多管齊下的方式同時進行。一般而言--注意僅僅是一般而言,我們只要一位女友就滿足了。只要一位就可以鄙視其他所有程式語言、所有編輯器、IDE 的單身工程師了!
一個成功就行,其他都可以不用管,這時就可以用 Promise.race。
Promise
.race([
告白服務1.告白(心儀妹子1, 我),
告白服務2.告白(心儀妹子2, 我),
告白服務3.告白(心儀妹子3, 我),
告白服務4.告白(心儀妹子4, 我),
告白服務5.告白(心儀妹子5, 我),
右手
])
.then(女友 => {
// ...
})
Promise.race 代表誰先成功誰上,可以接一個 array,裡頭同樣可以放 Value、Promise 和 Thenable 三種類型的值。畢竟每名妹子皆有不同,你不會期待每名妹子追的方式都一樣。如果是 Value 就表示直接成功,直接給你一個「女友」,Promise 和 Thenable 的話可能就要過五關斬六將,但不是不可能成功。
原則上如果有 value 類型,幾乎就可以肯定是這個了。此例中,如果前面用的告白函式都不是直接回傳一個「女友」的話,最後結果就一定會是「右手」。
Promise.race 也會回傳一個 Promise 物件,所以後面也可以串接 then,只要 array 裡有一個成功,這個 Promise 物件就會成功,並將該結果當成參數傳下去。
Note
如果有多個結果同時出來,那麼就會選擇 Array 中第一個成功結果。
但世界是很黑暗的,有時最大的問題不是對方拒絕,而是連拒絕都不願意給!所有的「告白服務」 都在那邊耗時間,石沉大海,不說行也不說不行,存心就要玩你!
這時就有一個簡單的技巧,就是另外加一個會 timeout 的 Promise 進去。
Promise
.race([
// ...
告白服務1.告白(心儀妹子1, 我),
告白服務2.告白(心儀妹子2, 我),
告白服務3.告白(心儀妹子3, 我),
告白服務4.告白(心儀妹子4, 我),
告白服務5.告白(心儀妹子5, 我),
// 這個 Promise 五秒後會回傳成功
new Promise(function (resolve, reject) {
setTimeout(function () {
resolve()
}, 5000)
});
])
.then(女友 => {
if (女友 === undefined) {
// 失敗了 T^T
}
})
這樣一來,至少五秒後就一定會看得到結果……雖然結果不一定是美妙的。
Note
話說回來,假設告白信送出去,還真有妹子願意答應你,而且還不只一位該怎麼辦?
想腳踏兩條船?
那你當然是死……不是啦,雖然目前原生 Promise 沒有「取消」的機制 (ES2015),但如果有這個需求,可以選用第三方提供的 Promise 函式庫,像是 Bluebird 就有提供取消 Promise 的機制。
這樣至少能在 Promise 還沒真的運行時取消,但假使「告白信」真的已經送出去了……那就請自求多福吧。
通通要搞定工具 - Promise.all
即使一切順利,告白成功,對方願意當你的女友,故事也還遠沒有結束。畢竟--搞定女友還要搞定她的家長啊!除此之外,她的親朋好友、乾哥乾姊等也都得解決。只要有一個沒成,結婚可能就沒戲。所以「準備」要齊全,做事才有把握。
這時 Promise.all 就可以上場了,用法與 Promise.race 相似:
Promise
.all([
// Promise or Thenable
告白服務1.告白(心儀妹子1, 我),
車商.買車(我, 所有錢),
房產服務.申請房產證(我, 借來的錢),
// Value
薪資條
])
// 之後的 then 同樣會以 array 的形式回傳每個 Promise 的值。
.then(([女友, 車, 房產, 薪資條]) => {
// ...
})
與 Promise.race 相同,Promise.all 也會回傳一個 Promise 物件,與 Promise.race 不同的地方在於 Promise.all 要等到所有的結果都成功才算成功,反之都算失敗。
另外 Promise.all 回傳的是整個 array 所有 Promise 和 Value 的最終結果。因此如果需要運行多個平行任務,並且要搜集結果回來時,就可以用 Promise.all 搜集所有的結果。
馬上得到結果工具 - Promise.resolve / Promise.reject
夢做完了,讓我們一起回到現實吧。
有些答案一開始就是注定的,有些問題不用等待,答案早已知曉,告白?明明都已經知道答案了,何必再問?
這時就可以使用 Promise.resolve 和 Promise.reject,兩者一樣會回傳一個 Promise 物件,但前者一開始就是成功的狀態,後者則一開始就是失敗的狀態。
Promise
.race([
Promise.reject(new Error('我只當你是哥哥')),
Promise.reject(new Error('我只把你當成朋友')),
Promise.reject(new Error('死變態')),
Promise.reject(new Error('色狼')),
Promise.resolve(右手)
])
當我們在學一件事物的時候要明白--這個世上並沒有銀彈。工具雖然能輔助我們解決問題,但問題的本質並不會因此變得簡單,頂多較容易操作而已。
「問題的本質是你只是死宅,僅此而已。」
「……不過沒關係,至少還有右手。」