首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JS中的Callback VS Promise

JS中的Callback VS Promise

作者头像
学前端
发布2020-04-07 15:40:26
4.9K0
发布2020-04-07 15:40:26
举报
文章被收录于专栏:前端之巅前端之巅

如果你不熟悉JavaScript,并且很难理解Promise的工作原理,希望本文能帮助您更清楚地了解Promise。话虽如此,本文针对的是那些对Promise不太熟悉的人。这篇文章不会讨论使用async / await执行Promise(尽管它们在功能上是一样的,但在大多数情况下 async/await 才是真正的语法糖)。

冷知识

实际上,在JavaScript原生之前,承诺就已经存在了一段时间。例如,在promises成为原生之前实现该模式的两个库是Q和when。

那么什么是Promise?Promise是JS对象,它们用于表示一个异步操作的最终完成 (或失败), 及其结果值.查看MDN 您可以通过使用回调方法或使用Promise执行异步操作来获得结果。但是两者之间有一些细微的差异。

CallBack 和Promise之间的区别

两者之间的主要区别在于,使用回调方法时,我们通常只是将回调传递给一个函数,该函数将在完成时被调用以获取某些结果。但是,在Promise中,您将回调附加在返回的Promise对象上。CallBacks:

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:

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的核心部分。

所以,我们为什么需要JS中的Promise?

为了明白这个问题,我们得先来聊聊为什么在大多数的JS开发者中,仅仅使用CallBack的方法是远远不够的。

回调地狱

使用回调方法的一个常见问题是,当我们最终不得不一次执行多个异步操作时,我们很容易以所谓的回调地狱告终,这可能会成为噩梦,因为它导致难以管理且难读取。换句话说,这是每个开发者的噩梦。

下面是一个简单的例子:


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)
  })

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)
  })

在https://github.com/tc39/proposal-promise-any了解更多信息。

成功/错误的处理技巧:

变化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) {
    //难不成你希望这儿一直出错吗?
  },
)
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-12-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一起学前端 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 冷知识
  • CallBack 和Promise之间的区别
  • Promise 对象
  • 回调地狱
  • 回调地狱发生的问题
    • 越来越难以阅读
      • 变得越来越难管理
      • Promise链
      • Promise 方法
      • Promise.all
      • Promise.race
      • Promise.allSettled
      • Promise.any
      • 成功/错误的处理技巧:
        • 变化1:
          • 变化2:
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档