【譯】JavaScript中的Promises

call_me_R發表於2019-05-02

你有沒有在JavaScript中遇到過promises並想知道它們是什麼?它們為什麼會被稱為promises呢?它們是否和你以任何方式對另一個人做出的承諾有關呢?

此外,你為什麼要使用promises呢?與傳統的JavaScript操作回撥(callbacks)相比,它們有什麼好處呢?

在本文中,你將學習有關JavaScript中promises的所有內容。你將明白它們是什麼,怎麼去使用它們,以及為什麼它們比回撥更受歡迎。

所以,promise是什麼?

promise是一個將來會返回值的物件。由於這種未來的東西,Promises非常適合非同步JavaScript操作。

如果你不明白非同步JavaScript意味著什麼,你可能還不適合讀這篇文章。我建議你回到關於callbacks這篇文章瞭解後再回來。

通過類比會更好地解析JavaScript promise的概念,所以我們來這樣做(類比),使其概念更加清晰。

想象一下,你準備下周為你的侄女舉辦生日派對。當你談到派對時,你的朋友,Jeff,提出他可以提供幫助。你很高心,讓他買一個黑森林(風格的)生日蛋糕。Jeff說可以。

在這裡,Jeff告訴你他會給你買一個黑森林生日蛋糕。這是約定好的。在JavaScript中,promise的工作方式和現實生活中的承諾一樣。可以使用以下方式編寫JavaScript版本的場景:

// jeffBuysCake is a promise
const promise = jeffBuysCake('black forest')
複製程式碼

你將學習如何構建jeffBuysCake。現在,把它當成一個promise

現在,Jeff尚未採取行動。在JavaScript中,我們說承諾(promise)正在等待中(pending)。如果你console.log一個promise物件,就可以驗證這點。

pending

列印jeffBuysCake表明承諾正在等待中。

當我們稍後一起構建jeffBuysCake時,你將能夠自己證明此console.log語句。

在與Jeff交談之後,你開始計劃下一步。你意識到如果Jeff信守諾言,並在聚會時買來一個黑森林蛋糕,你就可以按照計劃繼續派對了。

如果Jeff確實買來了蛋糕,在JavaScript中,我們說這個promise是實現(resolved)了。當一個承諾得到實現時,你會在.then呼叫中做下一件事情:

jeffBuysCake('black forest')
  .then(partyAsPlanned) // Woohoo! ???
複製程式碼

如果Jeff沒給你買來蛋糕,你必須自己去麵包店買了。(該死的,Jeff!)。如果發生這種情況,我們會說承諾被拒絕(rejected)了。

當承諾被拒絕了,你可以在.catch呼叫中執行應急計劃。

jeffBuysCake('black forest')
  .then(partyAsPlanned)
  .catch(buyCakeYourself) // Grumble Grumble... #*$%
複製程式碼

我的朋友,這就是對Promise的剖析了。

在JavaScript中,我們通常使用promises來獲取或修改一條資訊。當promise得到解決時,我們會對返回的資料執行某些操作。當promise拒絕時,我們處理錯誤:

getSomethingWithPromise()
  .then(data => {/* do something with data */})
  .catch(err => {/* handle the error */})
複製程式碼

現在,你知道一個promise如何運作了。讓我們進一步深入研究如何構建一個promise

構建一個promise

你可以使用new Promise來建立一個promise。這個Promise建構函式是一個包含兩個引數 -- resolvereject 的函式。

const promise = new Promise((resolve, reject) => {
  /* Do something here */
})
複製程式碼

如果resolve被呼叫,promise成功並繼續進入then鏈式(操作)。你傳遞給resolve的引數將是接下來then呼叫中的引數:

const promise = new Promise((resolve, reject) => {
  // Note: only 1 param allowed
  return resolve(27)
})

// Parameter passed resolve would be the arguments passed into then.
promise.then(number => console.log(number)) // 27
複製程式碼

如果reject被呼叫,promise失敗並繼續進入catch鏈式(操作)。同樣地,你傳遞給reject的引數將是catch呼叫中的引數:

const promise = new Promise((resolve, reject) => {
  // Note: only 1 param allowed
  return reject('???')
})

// Parameter passed into reject would be the arguments passed into catch.
promise.catch(err => console.log(err)) // ???
複製程式碼

你能看出resolvereject都是回撥函式嗎??

讓我們練習一下,嘗試構建jeffBuysCake promise。

首先,你知道Jeff說他會買一個蛋糕。那就是一個承諾。所以,我們從空promise入手:

const jeffBuysCake = cakeType => {
  return new Promise((resolve, reject) => {
    // Do something here
  })
}
複製程式碼

接下來,Jeff說他將在一週內購買蛋糕。讓我們使用setTimeout函式模擬這個等待七天的時間。我們將等待一秒,而不是七天:

const jeffBuysCake = cakeType => {
  return new Promise((resolve, reject) => {
    setTimeout(()=> {
      // Checks if Jeff buys a black forest cake
    }, 1000)
  })
}
複製程式碼

如果Jeff在一秒之後買了個黑森林蛋糕,我們就會返回promise,然後將黑森林蛋糕傳遞給then

如果Jeff買了另一種型別的蛋糕,我們拒接這個promise,並且說no cake,這會導致promise進入catch呼叫。

const jeffBuysCake = cakeType => {
  return new Promise((resolve, reject) => {
    setTimeout(()=> {
      if (cakeType
 - = 'black forest') {
        resolve('black forest cake!')
      } else {
        reject('No cake ?')
      }
    }, 1000)
  })
}
複製程式碼

讓我們來測試下這個promise。當你在下面的console.log記錄時,你會看到promise正在pedding(等待)。(如果你立即檢查控制檯,狀態將只是暫時掛起狀態。如果你需要更多時間檢查控制檯,請隨時將超時時間延長至10秒)。

const promise = jeffBuysCake('black forest')
console.log(promise)
複製程式碼

pending

列印jeffBuysCake表明承諾正在等待中。

如果你在promise鏈式中新增thencatch,你會看到black forest cake!no cake ?資訊,這取決於你傳入jeffBuysCake的蛋糕型別。

const promise = jeffBuysCake('black forest')
  .then(cake => console.log(cake))
  .catch(nocake => console.log(nocake))
複製程式碼

hascake

列印出來是“黑森林蛋糕”還是“沒有蛋糕”的資訊,取決於你傳入jeffBuysCake的(引數)。

建立一個promise不是很難,是吧??

既然你知道什麼是promise,如何製作一個promise以及如何使用promise。那麼,我們來回答下一個問題 -- 在非同步JavaScript中為什麼要使用promise而不是回撥呢?

Promises vs Callbacks

開發人員更喜歡promises而不是callbacks有三個原因:

  1. Promise減少了巢狀程式碼的數量
  2. Promise允許你輕鬆地視覺化執行流程
  3. Promise讓你可以在鏈式的末尾去處理所有錯誤

為了看到這三個好處,讓我們編寫一些JavaScript程式碼,它們通過callbackspromises來做一些非同步事情。

對於這個過程,假設你正在運營一個線上商店。你需要在客戶購買東西時向他收費,然後將他們的資訊輸入到你的資料庫中。最後,你將向他們傳送電子郵件:

  1. 向客戶收費
  2. 將客戶資訊輸入到資料庫
  3. 傳送電子郵件給客戶

讓我們一步一步地解決。首先,你需要一種從前端到後端獲取資訊的方法。通常,你會對這些操作使用post請求。

如果你使用ExpressNode,則初始化程式碼可能如下所示。如果你不知道任何NodeExpress(的知識點),請不要擔心。它們不是本文的主要部分。跟著下面來走:

// A little bit of NodeJS here. This is how you'll get data from the frontend through your API.
app.post('/buy-thing', (req, res) => {
  const customer = req.body

  // Charge customer here
})
複製程式碼

讓我們先介紹一下基於callback的程式碼。在這裡,你想要向客戶收費。如果收費成功,則將其資訊新增到資料庫中。如果收費失敗,則會丟擲錯誤,因此你的伺服器可以處理錯誤。

程式碼如下所示:

// Callback based code
app.post('/buy-thing', (req, res) => {
  const customer = req.body

  // First operation: charge the customer
  chargeCustomer(customer, (err, charge) => {
    if (err) throw err

    // Add to database here
  })
})
複製程式碼

現在,讓我們切換到基於promise的程式碼。同樣地,你向客戶收費。如果收費成功,則通過呼叫then將其資訊新增到資料庫中。如果收費失敗,你將在catch呼叫中自動處理:

// Promised based code
app.post('/buy-thing', (req, res) => {
  const customer = req.body

  // First operation: charge the customer
  chargeCustomer(customer)
    .then(/* Add to database */)
    .catch(err => console.log(err))
})
複製程式碼

繼續,你可以在收費成功後將你的客戶資訊新增到資料庫中。如果資料庫操作成功,則會向客戶傳送電子郵件。否則,你會丟擲一個錯誤。

考慮到這些步驟,基於callback的程式碼如下:

// Callback based code
app.post('/buy-thing', (req, res) => {
  const customer = req.body

  chargeCustomer(customer, (err, charge) => {
    if (err) throw err

    // Second operation: Add to database
    addToDatabase(customer, (err, document) => {
      if (err) throw err

      // Send email here
    })
  })
})
複製程式碼

對於基於promise的程式碼,如果資料庫操作成功,則在下一個then呼叫時傳送電子郵件。如果資料庫操作失敗,則會在最終的catch語句中自動處理錯誤:

// Promised based code
app.post('/buy-thing', (req, res) => {
  const customer = req.body

  chargeCustomer(customer)
    // Second operation: Add to database
    .then(_ => addToDatabase(customer))
    .then(/* Send email */)
    .catch(err => console.log(err))
})
複製程式碼

繼續最後一步,在資料庫操作成功時向客戶傳送電子郵件。如果成功傳送此電子郵件,則會有成功訊息通知到你的前端。否則,你丟擲一個錯誤:

以下是基於callback的程式碼:

app.post('/buy-thing', (req, res) => {
  const customer = req.body

  chargeCustomer(customer, (err, charge) => {
    if (err) throw err

    addToDatabase(customer, (err, document) => {
      if (err) throw err

      sendEmail(customer, (err, result) => {
        if (err) throw err

        // Tells frontend success message.
        res.send('success!')
      })
    })
  })
})
複製程式碼

然後,以下基於promise的程式碼:

app.post('/buy-thing', (req, res) => {
  const customer = req.body

  chargeCustomer(customer)
    .then(_ => addToDatabase(customer))
    .then(_ => sendEmail(customer) )
    .then(result => res.send('success!')))
    .catch(err => console.log(err))
})
複製程式碼

看看為什麼使用promises而不是callbacks編寫非同步程式碼要容易得多?你從回撥地獄(callback hell)一下子切換到了鏈式樂土上?。

一次觸發多個promises

promisescallbacks的另一個好處是,如果操作不依賴於彼此,則可以同時觸發兩個(或多個)promises,但是執行第三個操作需要兩個結果。

為此,你使用Promise.all方法,然後傳入一組你想要等待的promises。then的引數將會是一個陣列,其包含你promises返回的結果。

const friesPromise = getFries()
const burgerPromise = getBurger()
const drinksPromise = getDrinks()

const eatMeal = Promise.all([
  friesPromise,
  burgerPromise,
  drinksPromise
])
  .then([fries, burger, drinks] => {
    console.log(`Chomp. Awesome ${burger}! ?`)
    console.log(`Chomp. Delicious ${fries}! ?`)
    console.log(`Slurp. Ugh, shitty drink ${drink} ? `)
  })
複製程式碼

備註:還有一個名為Promise.race的方法,但我還沒找到合適的用例。你可以點選這裡去檢視。

最後,我們來談談瀏覽器支援情況!如果你不能在生產環境中使用它,那為什麼要學習promises呢。是吧?

瀏覽器支援Promise

令人興奮的訊息是:所有主流瀏覽器都支援promises

如果你需要支援IE 11及其以下版本,你可以使用Taylor Hakes製作的Promise Polyfill。它支援IE8的promises。?

結語

你在本文中學到了所有關於promises的知識。簡而言之,promises棒極了。它可以幫助你編寫非同步程式碼,而無需進入回撥地獄。

儘管你可能希望無論什麼時候都使用promises,但有些情況callbacks也是有意義的。不要忘記了callbacks啊?。

如果你有疑問,請隨時在下面發表評論,我會盡快回復你的。【PS:本文譯文,若需作者解答疑問,請移步原作者文章下評論】

感謝閱讀。這篇文章是否幫助到你?如果有,我希望你考慮分享它。你可能會幫助到其他人。非常感謝!

後話

原文:zellwk.com/blog/js-pro…

文章首發:github.com/reng99/blog…

更多內容:github.com/reng99/blog…

下一篇關於 async/await

相關文章