JavaScript 中的回调与承诺
在Medium上找到我
如果您是 JavaScript 新手,并且很难理解承诺的工作原理,希望本文能够帮助您更清楚地理解它们。
话虽如此,本文是针对那些对承诺的理解不太确定的人。
这篇文章不会讨论使用async/await执行承诺,尽管它们在功能上是相同的,但 async/await 在大多数情况下只是一种语法糖。
“什么”
实际上,Promise 在JavaScript 原生支持之前就已经存在了一段时间。例如,在 Promise 成为原生支持之前,就有两个库实现了这种模式: Q和when。
那么,什么是 Promise?JavaScript 对象中的 Promise表示异步操作最终完成或失败。您可以使用回调方法或 Promise 来执行异步操作并获取结果。但两者之间存在一些细微的差异。
回调和承诺之间的主要区别
两者之间的一个主要区别是,当使用回调方法时,我们通常只是将回调传递给一个函数,该函数将在完成时被调用以获取某些结果,而在承诺中,您将回调附加在返回的承诺对象上。
回调:
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)
承诺:
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 对象
我们刚才提到了承诺对象,因为它们是 JavaScript 中构成承诺的核心。
那么问题是,为什么我们需要 JavaScript 中的承诺?
好吧,为了更好地回答这个问题,我们必须问为什么使用回调方法对于大多数 JavaScript 开发人员来说还“不够” 。
回调地狱
使用回调方法的一个常见问题是,当我们最终必须一次执行多个异步操作时,我们很容易陷入所谓的回调地狱,这可能会成为一场噩梦,因为它会导致难以管理和难以阅读的代码 - 这是每个开发人员最可怕的噩梦。
以下是一个例子:
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)
})
你可以在代码片段中直观地看到,有一些奇怪的形状正在形成。仅仅从 3 个异步 API 调用开始,回调地狱就开始朝着与通常从上到下的方向相反的方向下沉。
有了承诺,它不再是一个问题,因为我们可以通过链接.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)
})
在回调代码片段中,如果我们嵌套更深的几个级别,事情就会变得丑陋且难以管理。
回调地狱引发的问题
只需查看我们之前代表这个“回调地狱”的代码片段,我们就可以列出由此出现的一系列危险问题,这些问题足以证明承诺是该语言的一个很好的补充:
- 阅读变得越来越困难
- 代码开始向两个方向移动(从上到下,然后从左到右)
- 管理变得越来越困难
- 由于代码嵌套得更深,所以不清楚发生了什么
- 我们必须始终确保不会意外地声明与外部作用域中已经声明的变量具有相同名称的变量(这称为阴影)
- 我们必须解决三个不同地点的 三种不同错误。
- 我们甚至不得不重命名每个错误,以确保不会掩盖其上方的错误。如果我们最终在这一系列操作中发出了额外的请求,我们就必须找到额外的变量名,确保它们不会与上方作用域中的错误冲突。
如果我们仔细观察这些例子,我们会注意到,大多数这些问题都是通过将承诺与链接起来而解决的.then
,我们将在下文讨论。
Promise 链
当我们需要执行一连串异步任务时,Promise 链式调用就变得非常有用。链式执行的每个任务只能在前一个任务完成后立即启动,并由.then
链中的 s 控制。
这些.then
块在内部设置,以便它们允许回调函数返回一个承诺,然后将其应用于.then
链中的每个块。
除了来自块的被拒绝的承诺之外,您返回的任何东西.then
最终都会成为已解决的承诺。.catch
这是一个简短的例子:
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 方法
JavaScript 中的 Promise 构造函数定义了几个静态方法,可用于从承诺中检索一个或多个结果:
Promise.all
当你想要累积一批异步操作并最终以数组的形式接收它们各自的值时,满足这一目标的promise方法之一Promise.all
就是。
Promise.all
当所有操作都成功时,收集操作结果。这与 类似,只是这里如果至少有一个操作失败,PromisePromise.allSettled
就会拒绝并抛出错误——最终会进入Promise 链的区块中。.catch
Promise 的拒绝可能发生在从操作开始到完成的任何时间点。如果拒绝发生在所有结果完成之前,那么那些未完成的结果最终将被中止,并且永远不会完成。换句话说,这是一种“全有”或“全无”的交易。
这是一个简单的代码示例,其中方法Promise.all
使用承诺,并在将结果存储到本地存储之前,在处理程序中以数组的形式检索结果:getFrogs
getLizards
.then
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
此方法返回一个承诺,只要可迭代对象中的一个承诺解决或拒绝,该承诺就会实现或拒绝,并带有该承诺的值或原因。
promise1
下面是和promise2
方法Promise.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)
})
这将产生这样的结果:
由于另一个承诺延迟了 200 毫秒,因此返回值最终为承诺拒绝。
Promise.allSettled
该Promise.allSettled
方法最终Promise.all
与上述方法的目标有些相似,不同之处在于,该方法不会在某个 Promise 失败时立即返回错误Promise.allSettled
,而是在所有给定的 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
是接受一个可迭代的承诺,并尝试返回一个从第一个给定的承诺中实现的承诺,或者如果所有给定的承诺都被拒绝,则返回一个拒绝的承诺AggregateError
,并保留拒绝原因。
这意味着,如果某个操作消耗了 15 个承诺,其中 14 个失败,而一个已解决,则结果Promise.any
将成为已解决的承诺的值:
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) {
// The error ends up here
})
然而,在变体 1 中,如果我们尝试在解析处理程序中抛出错误,我们将无法捕获该错误:
add(5, 5).then(
function success(result) {
throw new Error("You aren't getting passed me")
},
function error(error) {
// Oh no... you mean i'll never receive the error? :(
},
)
结论
这篇文章到此结束!希望你觉得这篇文章很有价值,并期待未来有更多精彩内容!
在Medium上找到我
文章来源:https://dev.to/jsmanifest/callbacks-vs-promises-in-javascript-2d5k