吳嘉豐 (Joseph) 已發佈 2019-10-19

來點 JavaScript 的 Promise

非同步與同步

什麼是同步非同步?想像你晚餐時,突然想到百貨公司的美食街吃東西。
這時候琳瑯滿目的餐點很多,你點了某家飲料,又點了某家的拉麵。這時候你各自拿到兩家的號碼牌,你可能就必須要同時注意兩家的店家的號碼牌顯示,看自己的餐點號碼是否好了。你可能利用時間滑手機,但是如果發現拉麵好了,就要趕快先去拿拉麵,如果發現飲料好了,就再去拿飲料,你並不能知道餐點實際完成的時間,以各自的速度去執行,這就是非同步,大家以各自的執行時間來計算,先完成的就先回應。

點飲料 -> 點拉麵 -> 滑手機 -> 拉麵好取餐 -> 飲料好取餐

在程式當中的非同步也是一樣,你執行了第一行 code,第二行 code,第三行,但第一、第二行 code 是 setTimeout 或 call ajax,這時候回傳的速度就不會是,1->2->3 行 code,而是先執行完的,先回應,如以下的範例:

function Ordering(){
  setTimeout(function(){
  console.log('飲料做好')
},3000)
  setTimeout(function(){
  console.log('拉麵做好')
},1000)
  console.log('滑手機') 
}

Ordering()
//滑手機
//拉麵做好
//飲料做好

這樣的好處是,並不會因為某件事,造成其他不相干的事情必須要等待這件事完成才執行,壞處是如果今天你做的第二件事必須要等待第一件事的回應完成,你才能拿第一件事的結果去做第二件事,那你有可能還沒拿到第一件事的結果,你就做了第二件事,導致錯誤發生。

譬如今天你到拉麵店想吃碗拉麵,因應現在拉麵店自動點餐的流程,所以你必須要先用自動點餐機點餐,拿到點餐單,確認點餐完成交給店員,等待有位子,店員將你要的拉麵端給你,然後享用了美味的拉麵。如果你漏了到自動點餐機點餐這一步,這時候廚師就不知道你要吃的是什麼,就算你這時候就去等位子,這時候就算有位子,店員也不知道你要吃的是什麼,你只是得到了需要重新點餐的回應,所以同步的情況就是用來確認這一步的結果回傳,再帶給下一步。

function Ordering(){
  var step = 0
  setTimeout(function(){
  step = 1
  console.log('step = '+step,'點餐完成')
},3000)
 if(step === 1){ //這時候 step = 0
  step = 2
  console.log('step = '+step,'等位子') }
}

Ordering() 
// 等到3秒後才會回傳 step = 1,"點餐完成"

看了以上的範例,發現 if(step === 1) 判斷點餐完成的是否成功的程式碼,在接收到的 step 其實是 0,因為步驟一點餐完成的 setTimeoput 還在等待回傳值時,if(step === 1) 就已經執行,這時候我們需要做的事就是確保每一步驟是同步的,a 完成再給 b,b完成再給 c。

點餐機點餐 (success) -> 等位子 (success) -> 享用美味的拉麵

callback function

回到吃拉麵的場景,這時候你可能想說要怎樣才能確保一個步驟成功再去執行下面的步驟呢?這時候就要設想一個場景,現在拉麵店因為人力不足改成自動取餐,所以當點餐完,有位子後,你就必須等拉麵煮好,再去出餐口取餐,可是因為又沒號碼牌,你又不可能一直在出餐口等你的餐點好,因為你也不知道你的順序,所以這時候聰明的店家便會給你一個震動的呼叫器,等餐點好,呼叫器一震動,你便會到出餐口取餐。而這個振動器呼叫的行為便是, callback function,他的作用是提醒目前的事件成功了,該去呼叫下一個事件了。

function order (process,callback){
  console.log(process)
  callback()
}

setTimeout(function(){order('等振動器提醒',function(){
    order('取餐完成',function(){
  })
  })},3000)

   // 等振動器提醒
   // 取餐完成

目前看起來這樣非同步的問題就解決了,變成可以同步了,But,就是這個 But,因為假設有 10 個 callback function 要呼叫的話,就會變成 callback hell。

傳說中的callback hell

圖片來源:Let's Get Out of the Callback Hell - JavaScript

為了解決 callback hell 的問題,於是產生了我們主題提到的 Promise

Promise

為了解決 callback hell 的問題,在 JavaScript ES6 時提出了 Promise,我們來看看 MDN 關於 Promise 的說明:

Promise 是一個表示非同步運算的最終完成或失敗的物件

表示透過 Promise 返回的值我們可以知道該物件是成功或失敗,
譬如以下的例子是原來透過 callback function 達成的成功或失敗:

function successCallback(result) {
  console.log(result);
}

function failureCallback(error) {
  console.log(error);
}

function getToken(sucess, fail){

  setTimeout(function(){
   var ifGetTlken = Math.random() // 是否振動器有提醒
   if(ifGetTlken > 0.5){
     sucess('順利取餐')
   }else{
     fail('取不到餐')
   }

  },3000)

}
getToken()

//執行結果:
// '順利交棒'

現在新的 Promise 可以改成下面這樣:

function getToken(){
return new Promise(function(resolve, reject) {
  const states = Math.random() > 0.5 ? true : false
  if(states){
  setTimeout(function() {
    resolve('順利取餐');
  }, 3000);}else{
    setTimeout(function() {
    reject('取不到餐');
  }, 3000);
  }
});}

getToken()
  .then((res)=>console.log(res)) // 順利取餐
  .catch((err)=>console.log(err)) // 取不到餐

其中 new Promise 代表的是建立一個 Promise 物件,function(resolve, reject){} 代表的是建構式裡面包含的執行函式( executor function ),執行函式包含,resolve、reject,這兩個函式作為參數,當事件成功時便會回傳 resolve 裡面的值,反之當事件失敗便會回傳 reject 裡面的值。

getToken().thenthen 代表了可以接收 getToken() 這個 Promise 完成時,可以接受到回傳的值,如果回傳的值為錯誤時,則可以用 .catch 去做錯誤的值接收,.then 是可以一直串接下去的。這個行為叫做 Promise Chain

var getToken = function(data) {
console.log(data)
return new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve('scuess');
  }, 3000);
})}

getToken('點餐')
.then((res)=>{console.log(res)
  return getToken('拿到餐點')} 
) 
.then((res)=>console.log(res))
.catch((err)=>console.log(err))

來源 MDN:Promise

來源 MDN:Promise

而 Promise 的執行過程如上圖,一開始執行 Promise 直到回應前就是 pedning,等執行結果回應,依照成功就是 fulfill 或是失敗就是 reject,如果 fulfill 就可以串 .then 的鍊得到回應的結果, reject 的話就是串 .then 和 .catch 得到錯誤的回應,再來就是 return 一個新的 Promise,可以在經歷上面的過程。所以 .then 才可以和 .then 串接起來。

然常用的 Promise API 還有以下的 API:

  • Promise.all()
  • Promise.race()

Promise.all()

Promise.all 的用法就是確認所有 Promise 都已經 reslove 或當中包含的不是 Promise 的 物件也實現,才會回應結果。還有一種情況,就是 Promise all 當中有一個 reject 出現時,也會回應。就像是你參加一個團體的馬拉松,當團體抵達時,才會去計算團隊的成績,而當有個成員受傷時,也代表了這個團體的比賽已經失敗。

var promise1 = Promise.resolve(11);
var promise2 = 46;
var promise3 = new Promise(function(resolve, reject) {
  setTimeout(resolve, 100, '成功');
});

Promise.all([promise1, promise2, promise3]).then(function(values) {
  console.log(values);
}); //[11, 46, "成功"]

Promise.race()

Promise.race 的用法就是確認傳入的 Promise Array 當中,只要有一個 Promise reslove 或 reject,Promise.race 就會回傳。

var promise1 = Promise.resolve(11);
var promise2 = 46;
var promise3 = new Promise(function(resolve, reject) {
  setTimeout(resolve, 100, '成功');
});

Promise.race([promise1, promise2, promise3]).then(function(values) {
  console.log(values);
}); //11

參考資料

https://ithelp.ithome.com.tw/articles/10194569
https://blog.techbridge.cc/2019/10/05/javascript-async-sync-and-callback/
https://developer.mozilla.org/zh-TW/docs/Mozilla/JavaScript_code_modules/Promise.jsm/Promise
https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Promise
https://developers.google.com/web/fundamentals/primers/promises?hl=zh-tw
https://cythilya.github.io/2018/10/31/promise/
https://cythilya.github.io/2018/10/30/callback/

關於筆者

暱稱:吳嘉豐 (Joseph)

介紹:轉職前端,喜歡研究前端技術的工程師,平時出沒在各個前端交流的社群

linkedin

文章列表 文章列表