[譯]使用JavaScript Promise的優勢

本文譯自@Mohsan Riaz發表在medium的文章,已獲其受權,原文地址見傳送門javascript

在Javascript中編寫異步代碼經常使人不安,尤爲是在使用continuation-passing style(CPS)風格的時候。異步代碼影響代碼的可讀性,使代碼很難解耦爲更小的獨立代碼塊,致使其更易出錯,且之後很難改動。java

譯者注:continuation-passing style,簡單來講就是函數的返回不依靠return語句,而是依靠額外傳入一個回調函數,將返回值做爲回調函數的參數來傳出。具體可參考這篇博客編程

若是有一種能夠以相似同步代碼的風格來寫異步代碼的方式,那咱們的編程生涯確定會更加的愉快。使用JavaScript的promise能夠作到這一點。promise

讓咱們先開始嘗試找找使用callback有什麼問題。markdown

Callbacks

例如:若是須要獲取一個國家的列表,緊接着獲取國家列表中第一個國家的的城市列表,而後緊接着獲取這個城市列表中的全部大學,最終展現第一個城市的第一所大學。異步

因爲每次調用都相互依賴於上一次調用的結果,須要調用一系列的嵌套的回調函數。ide

function fetchCountries(){
  fetchJSON("/countries", (success, countries) => {
    if(success){
      try{
        // do some stuff
        fetchCities(countries[0].id);
      } catch(e){
          processError();
      }
    }else
      processError();
  });
}

function fetchCities(countryId){
  fetchJSON(`countries/${countryId}/cities`, (success, cities) => {
    if(success){
      try{
        // do some stuff
        fetchUniversities(cities[0].id);
      } catch(e){
          processError()
      }
    }else
      processError();
  });
}

function fetchUniversities(cityId){
  fetchJSON(`cities/${cityId}/universities`, (success, universities) => {
    if(success){
      try{
        // do some stuff
        fetchUniversityDetails(universities[0].id);
      }catch(e){
        processError();
      }
    }else
      processError();
  });
}

function fetchUniversityDetails(univId){
  fetchJSON(`/universities/${univId}`, (success, university) => {
    if(success){
      try{
        // do some stuff
        hideLoader();
      }catch(e){
        processError();
      }
    }else
      processError();
    
  });
}
複製代碼

上面這個實現有什麼問題?函數

回調函數沒有將異步邏輯放在同一個位置,而是決策接下來調用哪一個函數。簡而言之,咱們將控制流給了各個函數,讓他們緊密的耦合到了一塊兒。oop

爲何用Promise

在我看來,與簡單的回調處理(continuation-passing style)相比,promise有4個主要優勢:fetch

  1. 更好的定義異步邏輯控制流
  2. 解耦
  3. 更好的錯誤處理
  4. 提高可讀性

Promise的使用

Javascript的Promise讓異步代碼像同步代碼同樣return一個值,這個返回值是個能承諾成功或失敗值的object。這個小改動讓它用起來很是強大。

get("/countries")
  .then(populateContDropdown)
  .then(countries =>  get(`countries/${countries[0].id}/cities`))
  .then(populateCitiesDropdown)
  .then(cities => get(`cities/${cities[0].id}/universities`))
  .then(populateUnivDropdown)
  .then(universities => get(`universities/${universities[0].id}`))
  .then(populateUnivDetails)
  .catch(showError)
  .then(hideLoader)
複製代碼

與同步編程類似,一個函數的輸出是下一個函數的輸入,在調用鏈中像使用JSON.parse同樣使用JS函數,他們的返回值會被餵給下一個回調函數。

相似JavaScript的try/catch,若是有異常(exception),catch函數會將其捕獲,後面的代碼會照常運行,上面例子中的loader會在執行後隱藏。

對比

咱們在同一個地方定義全部異步邏輯,沒必要作額外的檢查或者爲錯誤處理使用try/catch。最終,代碼具備更高的可讀性,更低的耦合度,可複用的獨立函數。

錯誤處理的細節

不管什麼時候出現有意或無心的異常,都會在下一個catch處理程序中拋出異常。你能夠將catch處理程序放置在鏈中的任何位置以捕獲特定的異常,此時能夠顯示一個錯誤或者從新取值。

get("/countries")
  .then(JSON.parse)
  .catch(e => console.log("Could not parse string"))
  .then(...)
  .catch(e => console.log("Error occured"))
複製代碼

在上面的例子中,第二個console語句將不會被打印。

若是一個異常已被捕獲且想將其傳到下一個catch處理函數中,必須將其拋出。

若是想展現多個錯誤信息這樣就很方便,例如:

get("/coutries")
  .then(...)
  .catch(e => {
    showLowLevelError(); 
    throw e;
  })
  .catch(showHighLevelError)
複製代碼

Promise vs Event listener

若是一個事件但願能被屢次觸發,事件監聽器就很是適用,例如按鈕點擊事件。Promise和事件監聽器很類似,但他們有兩點本質的不一樣:

  1. 不管是異常仍是正常值Promise都只會決議一次。
  2. 即便在Promise決議後添加的回調函數任然會被調用,這意味着能夠稍後檢查Promise是fullfiled仍是rejected

這頗有用,由於咱們對發生的結果作出反應更感興趣。舉個圖片預加載的簡單例子:

var imagePromise = preloadImage("src.png");
setTimeout(() => {
  imagePromise.then(() => console.log("image was loaded") )
     .catch(()=> console.log("could not load the image"))
}, 2000)

function preloadImage (path) {
  return new Promise((resolve, reject) => {
    var image = new Image();
    image.onload  = resolve;
    image.onerror = reject;
    image.src = path;
  });
};
複製代碼

Promise.all([...])對於批量決議頗有用

Promise.all([imagePromise1, imagePromise2, ....])
  .then(...)
    .catch(...)
複製代碼

總結

在JavaScript中的大部分實踐中,Promise很是有用,尤爲是成功或者失敗回調函數只能被執行一次。但在一些實踐中,尤爲是事件回調函數會被屢次調用的實踐中,普通的回調函數更好。感謝你閱讀這篇文章,若是有什麼問題,請在評論區留言。

初次翻譯,若有問題歡迎指正。

相關文章
相關標籤/搜索