大類的技術手記

淺談 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(右手)
    ])

當我們在學一件事物的時候要明白--這個世上並沒有銀彈。工具雖然能輔助我們解決問題,但問題的本質並不會因此變得簡單,頂多較容易操作而已。

「問題的本質是你只是死宅,僅此而已。」

「……不過沒關係,至少還有右手。」

,,
,