JS中的CallBack與Promise間的對比

JS中 CallBack VS Promise

若是您不熟悉JavaScript,而且很難理解Promise的工做原理,但願本文能幫助您更清楚地瞭解Promise。 話雖如此,本文針對的是那些對Promise不太熟悉的人。 這篇文章不會討論使用async / await執行Promise(儘管它們在功能上是同樣的,但在大多數狀況下 async/await 纔是真正的語法糖)。javascript

冷知識

實際上,在JavaScript原生以前,承諾就已經存在了一段時間。例如,在promises成爲原生以前實現該模式的兩個庫是Qwhenjava

那麼什麼是Promise?Promise是JS對象,它們用於表示一個異步操做的最終完成 (或失敗), 及其結果值.查看MDN 您能夠經過使用回調方法或使用Promise執行異步操做來得到結果。可是二者之間有一些細微的差別。node

CallBack 和Promise之間的區別

二者之間的主要區別在於,使用回調方法時,咱們一般只是將回調傳遞給一個函數,該函數將在完成時被調用以獲取某些結果。可是,在Promise中,您將回調附加在返回的Promise對象上。 CallBacks:git

function getMoneyBack(money, callback) {
  if (typeof money !== 'number') {
    callback(null, new Error('money is not a number'))
  } else {
    callback(money)
  }
}

const money = getMoneyBack(1200)
console.log(money)
複製代碼

Promises:github

function getMoneyBack(money) {
  return new Promise((resolve, reject) => {
    if (typeof money !== 'number') {
      reject(new Error('money is not a number'))
    } else {
      resolve(money)
    }
  })
}

getMoneyBack(1200).then((money) => {
  console.log(money)
})
複製代碼

Promise 對象

它們是JS中構成Promise的核心部分。api

因此,咱們爲何須要JS中的Promise?數組

爲了明白這個問題,咱們得先來聊聊爲何在大多數的JS開發者中,僅僅使用CallBack的方法是遠遠不夠的。promise

回調地獄

使用回調方法的一個常見問題是,當咱們最終不得不一次執行多個異步操做時,咱們很容易以所謂的回調地獄了結,這可能會成爲噩夢,由於它致使難以管理且難讀取。換句話說,這是每一個開發者的噩夢。異步

下面是一個簡單的例子:async

function getFrogsWithVitalSigns(params, callback) {
  let frogIds, frogsListWithVitalSignsData
  
  api.fetchFrogs(params, (frogs, error) => {
    if (error) {
      console.error(error)
      return
    } else {
      frogIds = frogs.map(({ id }) => id)
      // The list of frogs did not include their health information, so lets fetch that now
      api.fetchFrogsVitalSigns(
        frogIds,
        (frogsListWithEncryptedVitalSigns, err) => {
          if (err) {
            // do something with error logic
          } else {
            // The list of frogs health info is encrypted. Our friend texted us the secret key to use in this step. This is used to decrypt the list of frogs encrypted health information
            api.decryptFrogsListVitalSigns(
              frogsListWithEncryptedVitalSigns,
              'pepsi',
              (data, errorr) => {
                if (errorrr) {
                  throw new Error('An error occurred in the final api call')
                } else {
                  if (Array.isArray(data)) {
                    frogsListWithVitalSignsData = data
                  } else {
                    frogsListWithVitalSignsData = data.map(
                      ({ vital_signs }) => vital_signs,
                    )
                    console.log(frogsListWithVitalSignsData)
                  }
                }
              },
            )
          }
        },
      )
    }
  })
}

const frogsWithVitalSigns = getFrogsWithVitalSigns({
  offset: 50,
})
  .then((result) => {
    console.log(result)
  })
  .catch((error) => {
    console.error(error)
  })
複製代碼

你能夠在代碼段中直觀地看到有一些奇怪的結果。僅經過三個異步API調用,回調地獄就開始陷入與一般的上下方向相反的方向。 有了promise,它再也不成爲問題,由於咱們能夠經過連接.then的方法將代碼保留在第一個處理程序的根目錄中:

function getFrogsWithVitalSigns(params, callback) {
  let frogIds, frogsListWithVitalSignsData
  
  api
    .fetchFrogs(params)
    .then((frogs) => {
      frogIds = frogs.map(({ id }) => id)
      // The list of frogs did not include their health information, so lets fetch that now
      return api.fetchFrogsVitalSigns(frogIds)
    })
    .then((frogsListWithEncryptedVitalSigns) => {
      // The list of frogs health info is encrypted. Our friend texted us the secret key to use in this step. This is used to decrypt the list of frogs encrypted health information
      return api.decryptFrogsListVitalSigns(
        frogsListWithEncryptedVitalSigns,
        'pepsi',
      )
    })
    .then((data) => {
      if (Array.isArray(data)) {
        frogsListWithVitalSignsData = data
      } else {
        frogsListWithVitalSignsData = data.map(
          ({ vital_signs }) => vital_signs,
        )
        console.log(frogsListWithVitalSignsData)
      }
    })
    .catch((error) => {
      console.error(error)
    })
  })
}

const frogsWithVitalSigns = getFrogsWithVitalSigns({
  offset: 50,
})
  .then((result) => {
    console.log(result)
  })
  .catch((error) => {
    console.error(error)
  })
複製代碼

在這個回調代碼段中,若是咱們僅嵌套幾層,那麼事情將變得更加難以管理。

回調地獄發生的問題

僅經過查看錶明此回調地獄的先前代碼片斷,咱們就能夠得出一系列由此而產生的危險問題,這些清單足以證實promise是該語言的不錯補充:

愈來愈難以閱讀

代碼開始向兩個方向移動(從上到下,而後從左到右)

變得愈來愈難管理

  • 目前尚不清楚將代碼嵌套得更深時發生了什麼
  • 咱們始終必須確保咱們不會意外地聲明與外部做用域中已經聲明的變量名稱相同的變量(這被稱爲shadowing)
  • 咱們不得不考慮在三個不一樣位置的三個不一樣錯誤
  • 咱們甚至不得不重命名每一個錯誤,以確保咱們不會將錯誤覆蓋在錯誤之上。若是咱們最終在這一系列操做中提出了其餘要求,咱們將不得不找到其餘變量名稱,這些名稱最終不會與上述範圍內的錯誤發生衝突。 若是咱們仔細研究這些示例,咱們會注意到,大多數問題都是經過可以與.then連接Promise而解決的。

Promise鏈

當咱們須要執行一系列異步任務時,承諾鏈就變得絕對有用。被連接的每一個任務只能在上一個任務完成後當即開始,由.then鏈的s 控制。

這些.then塊是在內部設置的,所以它們容許回調函數返回promise,而後將其應用於.then鏈中的每一個塊.

.then除了.catch塊帶來的被拒絕的Promise外,您從中返回的任何東西最終都會變成一個正常的Promise。 這是一個簡短的示例:

const add = (num1, num2) => new Promise((resolve) => resolve(num1 + num2))

add(2, 4)
  .then((result) => {
    console.log(result) // result: 6
    return result + 10
  })
  .then((result) => {
    console.log(result) // result: 16
    return result
  })
  .then((result) => {
    console.log(result) // result: 16
  })
複製代碼

Promise 方法

JS中的Promise構造函數定義了幾種靜態方法,可用於從Promise中檢查一個或者多個結果

Promise.all

當你想要累計一批異步操做並最終將它們的每個值做爲一個數組來接收時,知足此目標的Promise方法就是Promise.all

Promise.all可以在全部操做成功結束時,蒐集操做結構。這僅在此處相似於Promise.allSettled 。若是這些操做中的某一項或者多項失敗,則Promise將拒絕並顯示錯誤。最終,這會出如今.catchPromise 鏈中。

從操做開始到完成的任什麼時候候均可能發生Promise拒絕。若是在全部結果完成以前發生拒絕,那麼未完成的結果將被終止,而且永遠沒法完成。換句話說,它是全有或全無的調用之一。

這是一個簡單的代碼示例,其中該Promise.all方法使用getFrogsgetLizards,它們是promises。再將結果.then存儲到LocalStarage以前,它將在處理程序中以數組形式檢索結果:

const getFrogs = new Promise((resolve) => {
  resolve([
    { id: 'mlo29naz', name: 'larry', born: '2016-02-22' },
    { id: 'lp2qmsmw', name: 'sally', born: '2018-09-13' },
  ])
})

const getLizards = new Promise((resolve) => {
  resolve([
    { id: 'aom39d', name: 'john', born: '2017-08-11' },
    { id: '20fja93', name: 'chris', born: '2017-01-30' },
  ])
})

function addToStorage(item) {
  if (item) {
    let prevItems = localStorage.getItem('items')
    
    if (typeof prevItems === 'string') {
      prevItems = JSON.parse(prevItems)
    } else {
      prevItems = []
    }
    
    const newItems = [...prevItems, item]
    localStorage.setItem('items', JSON.stringify(newItems))
  }
}

let allItems = []

Promise.all([getFrogs, getLizards])
  .then(([frogs, lizards]) => {
    localStorage.clear()
    
    frogs.forEach((frog) => {
      allItems.push(frog)
    })
    
    lizards.forEach((lizard) => {
      allItems.push(lizard)
    })
    
    allItems.forEach((item) => {
      addToStorage(item)
    })
  })
  .catch((error) => {
    console.error(error)
  })
  
console.log(localStorage.getItem('items'))
/* result: [{"id":"mlo29naz","name":"larry","born":"2016-02-22"},{"id":"lp2qmsmw","name":"sally","born":"2018-09-13"},{"id":"aom39d","name":"john","born":"2017-08-11"},{"id":"20fja93","name":"chris","born":"2017-01-30"}] */
複製代碼

Promise.race

每當可迭代的Promise中的一個Promise以該Promise的值或緣由解析或拒絕時,此方法都會返回一個履行或拒絕的Promise。

這裏是promise1promise2Promise.race之間的有效方法的簡單例子:

const promise1 = new Promise((resolve) => {
  setTimeout(() => {
    resolve('some result')
  }, 200)
})

const promise2 = new Promise((resolve, reject) => {
  reject(new Error('some promise2 error'))
})

Promise.race([promise1, promise2])
  .then((result) => {
    console.log(result)
  })
  .catch((error) => {
    console.error(error)
  })
複製代碼

它的結果將是:

因爲另外一個Promise被延遲了200毫秒,所以返回值最終成爲了Promise拒絕。

Promise.allSettled

Promise.allSettled方法有些相似於Promise.all共享一個類似的目標,除了在一個Promise失敗時它不會當即拒絕產生錯誤,而是會返回一個Promise,這個Promise將會在全部給定的Promise都已解決或被拒絕後最終解決,並將結果累積到每一個項目表明其promise操做的結果的數組。

這意味着您將老是以數組數據類型結束。這是一個實際的例子:

const add = (num1, num2) => new Promise((resolve) => resolve(num1 + num2))

const multiply = (num1, num2) => new Promise((resolve) => resolve(num1 * num2))

const fail = (num1) =>
  new Promise((resolve, reject) =>
    setTimeout(() => reject(new Error('You, my friend, were too late')), 200),
  )
  
const fail2 = (num1) =>
  new Promise((resolve, reject) =>
    setTimeout(
      () => reject(new Error('Being late is never a good habit')),
      100,
    ),
  )
  
const promises = [add(2, 4), multiply(5, 5), fail('hi'), fail2('hello')]

Promise.allSettled(promises)
  .then((result) => {
    console.log(result)
  })
  .catch((error) => {
    console.error(error)
  })
複製代碼

img call-2

Promise.any

Promise.any是添加到Promise構造函數的提案,該提案目前處於TC39流程的第3階段。 Promise.any建議的是接受一個可迭代的Promise,並試圖返回這與多數同意接受或拒絕的Promise。 這意味着若是有一個操做消耗了15個Promise, 而其中的14 個在解決一個Promise時就失敗了,那麼結果將Promise.any成爲已解決的Promise的值:

const multiply = (num1, num2) => new Promise((resolve) => resolve(num1 * num2))

const fail = (num1) =>
  new Promise((resolve, reject) =>
    setTimeout(() => reject(new Error('You, my friend, were too late')), 200),
  )
  
const promises = [
  fail(2),
  fail(),
  fail(),
  multiply(2, 2),
  fail(2),
  fail(2),
  fail(2, 2),
  fail(29892),
  fail(2),
  fail(2, 2),
  fail('hello'),
  fail(2),
  fail(2),
  fail(1),
  fail(),
]

Promise.any(promises)
  .then((result) => {
    console.log(result) // result: 4
  })
  .catch((error) => {
    console.error(error)
  })
複製代碼

此處瞭解更多信息。

成功/錯誤的處理技巧:

變化1:

add(5, 5).then(
  function success(result) {
    return result
  },
  function error(error) {
    console.error(error)
  },
)
複製代碼

變化2:

add(5, 5)
  .then(function success(result) {
    return result
  })
  .catch(function(error) {
    console.error(error)
  })
複製代碼

可是,這兩個例子並不徹底相同,在變化2中,若是咱們嘗試在resolve處理程序中發生了錯誤,那麼咱們只要檢查.catch的內容有沒有出錯:

add(5, 5)
  .then(function success(result) {
    throw new Error("You aren't getting passed me")
  })
  .catch(function(error) {
    // 錯誤在這兒中止了
  })
複製代碼

在變化1中,若是咱們試圖拋出一個錯誤的處理程序,咱們可能找不到錯誤所在:

add(5, 5).then(
  function success(result) {
    throw new Error("You aren't getting passed me")
  },
  function error(error) {
    //難不成你但願這兒一直出錯嗎?
  },
)
複製代碼

若是你有什麼疑問歡迎在下面評論區留言!

相關文章
相關標籤/搜索