Promise使用过程中常见错误及避坑指南
JS 写的任何东西都会被执行并且会起作用,所以当我们追求高质量代码时,它应该是快速且无错误的。在使用 Promises时,我们可能很容易地犯一些错误,比如忘记输入代码的某些部分或使用不正确的东西。下面列出了开发过程中常见的 Promise 问题。
1、嵌套Promise
检查下面的代码
loadSomething().then(something => {
loadAnotherThing().then(another => {
doSomething(something, another)
}).catch(e => console.error(e))
}).catch(e => console.error(e))
Promises 一个重要的作用就是为了修复“回调地狱”,上面的例子是用“回调地狱”风格编写的。要正确地重写代码,我们需要理解为什么原始代码是这样写的。在上述情况下,是需要在两个 Promise 的结果可用后做一些事情,因此进行了嵌套。可以使用Promise.all()
重写它:
Promise.all([loadSomething(), loadAnotherThing()])
.then(([something, another]) => {
doSomething(something, another)
})
.catch(e => console.error(e))
检查错误处理。当正确使用 Promises 时,只需要一个catch()
。
Promise 链还为我们提供了一个finally()
处理程序。它总是被执行,它有利于清理和一些总是被执行的最终任务,无论 Promise 被解决还是被拒绝。如下:
Promise.all([loadSomething(), loadAnotherThing()])
.then(([something, another]) => {
doSomething(something, another)
})
.catch(e => console.error(e))
.finally(() => console.log('Promise executed'))
2、断裂的Promise链
Promise 使用方便的主要原因之一是“promise-chaining”(链式结构),一种将 Promise 的结果传递到链中并在链的末端调用catch以在一个地方捕获错误的能力。让我们看看下面的例子:
function anAsyncCall() {
const promise = doSomethingAsync()
promise.then(() => {
somethingElse()
})
return promise
}
上面代码的问题是执行somethingElse()
方法时,若该段内发生的错误将会丢失。那是因为我们没有从somethingElse()
方法中返回 Promise。默认情况下,.then()
总是返回一个 Promise,因此,在我们的例子中,返回的 Promise 将不会被使用,并且.then()
返回一个新的 Promise。我们丢失了方法somethingElse()
的执行结果。我们可以将其重写为
function anAsyncCall() {
return doSomethingAsync()
.then(somethingElse)
.catch(e => console.error(e))
}
3、在 Promise 链中混合使用同步和异步代码
这是 Promises 最常见的错误之一。人们倾向于对所有事物使用 Promises 然后将它们链接起来,即使对于异步代码也是如此。在一个地方进行错误处理可能更容易,但是为此使用 Promise 链并不是正确的方法。你只会占用内存并且给垃圾收集器带来更多的工作。检查下面的例子
const fetch = require('node-fetch') // only when running in Node.js
const getUsers = fetch('https://api.github.com/users')
const extractUsersData = users =>
users.map(({ id, login }) => ({ id, login }))
const getRepos = users => Promise.all(
users.map(({ login }) => fetch(`https://api.github.com/users/${login}/repos`))
)
const getFullName = repos => repos.map(repo => ({ fullName: repo.full_name }))
const getDataAndFormatIt = () => {
return getUsers()
.then(extractUsersData)
.then(getRepos)
.then(getFullName)
.then(repos => console.log(repos))
.catch(error => console.error(error))
}
getDataAndFormatIt()
在这个例子中,我们在 Promise 链中混合了同步和异步代码。其中两种方法是从 GitHub 获取数据,另外两种只是通过数组映射并提取一些数据,最后一种只是在控制台中记录数据(这三种方法都是同步的)。这是人们常犯的错误,尤其是当您必须获取一些数据,然后为获取的数组中的每个元素获取更多数据时,但这是错误的。通过将getDataAndFormatIt()
方法改成async/await,可以很容易的看出错误在哪里:
const getDataAndFormatIt = async () => {
try {
const users = await getUsers()
const userData = await extractUsersData(users)
const repos = await getRepos(userData)
const fullName = await getFullName(repos)
await logRepos(fullName)
} catch (error) {
console.error(error)
}
}
我们将每个方法都视为异步(这就是 Promise 链示例中会发生的情况)。但是我们不需要每个方法都需要 Promises,只需要两个异步方法,因为其他三个是同步的。通过稍微重写代码,将最终解决内存问题:
const getDataAndFormatIt = async () => {
try {
const users = await getUsers()
const userData = extractUsersData(users)
const repos = await getRepos(userData)
const fullName = getFullName(repos)
logRepos(fullName)
} catch (error) {
console.error(error)
}
}
只有异步的方法会作为 Promises 执行,其他的会作为同步方法执行。我们不再有内存泄漏。所以应该避免链接 同步
和 异步
方法,这会在将来产生很多问题(尤其是当您有大量数据要处理时)。为了简便起见,请选择 async/await,它将帮助您了解 Promise 应该是什么,以及同步方法中应该保留什么。
4、丢失catch
JavaScript 不强制执行错误处理。每当程序员忘记捕获错误时,JavaScript 代码就会引发运行时异常。但是,回调语法使错误处理更加直观。每个回调函数接收两个参数,error
和result
。通过编写代码,您将始终看到未使用的error
变量,并且您需要在某个时候处理它。检查下面的代码:
fs.readFile('foo.txt', (error, result) => {
if (error) {
return console.error(error)
}
console.log(result)
})
由于回调函数签名有一个error
,处理它变得更加直观,并且更容易发现缺少的错误处理程序。Promises 很容易忘记捕获错误,因为它.catch()
是可选的,而.then()
我对单个成功处理程序非常满意。这将发出UnhandledPromiseRejectionWarning 在 Node.js 中,可能会导致内存或文件描述符泄漏。Promise 回调中的代码占用一些内存,在 Promise 被解析或拒绝后清理已用内存应该由垃圾收集器完成。但如果我们不正确处理被拒绝的承诺,那可能不会发生。如果我们访问一些 I/O 源或在 Promise 回调中创建变量,将创建一个文件描述符并使用内存。如果没有正确处理 Promise 拒绝,内存将不会被清理,文件描述符也不会被关闭。这样做数百次,您将导致大量内存泄漏,并且其他一些功能可能会失败。为避免进程崩溃和内存泄漏,始终以.catch()
.
如果您尝试运行下面的代码,它将失败并显示UnhandledPromiseRejectionWarning:
const fs = require('fs').promises
fs.stat('non-existing-file.txt')
.then(stat => console.log(stat))
如果您没有正确处理您的错误,您将泄漏一个文件描述符或进入其他一些拒绝服务的情况。这就是为什么您需要在 Promise 被拒绝时正确清理所有内容。为了正确处理它,我们应该.catch()
在这里添加一条声明:
const fs = require('fs').promises
fs.stat('non-existing-file.txt')
.then(stat => console.log(stat))
.catch(error => console.error(error)
用一个.catch()
语句,当错误发生时,我们应该在控制台中记录它:
{ [Error: ENOENT: no such file or directory, stat 'non-existing-file.txt']
errno: -2,
code: 'ENOENT',
syscall: 'stat',
path: 'non-existing-file.txt' }
5、忘记返回一个Promise
在下面的代码中,我们忘记在成功处理程序中返回 Promise getUserData()
getUser()
.then(user => {
getUserData(user)
})
.then(userData => {
// userData is not defined
})
.catch(e => console.error(e))
结果,userData
它是未定义的。此外,此类代码可能会导致unhandledRejection错误。正确的代码应该是这样的:
getUser()
.then(user => {
return getUserData(user)
})
.then(userData => {
// userData is defined
})
.catch(e => console.error(e))
6、Promise中使用同步代码
Promises 旨在帮助您管理异步代码。因此,使用 Promises 进行同步处理没有任何优势。根据 JavaScript 文档:“Promise 对象用于延迟和异步计算。Promise 表示尚未完成但预计在未来完成的操作。” 如果我们将同步操作包装在 Promise 中,会发生什么情况,如下所示
const syncPromise = new Promise((resolve, reject) => {
传递给 Promise 的函数将立即被调用,但解决方案将被安排在微任务队列中,就像任何其他异步任务一样。它只会在没有特殊原因的情况下阻塞事件循环。在上面的例子中,我们创建了一个额外的上下文,我们没有使用它。这将使我们的代码变慢并消耗额外的资源,没有任何好处。此外,由于我们的函数是 Promise,JavaScript 引擎将跳过旨在减少函数调用开销的最重要的代码优化之一——自动函数内联。
7、混合Promise 和 Async/Await
const mainMethod = () => {
return new Promise(async function (resolve, reject) {
try {
const data1 = await someMethod()
const data2 = await someOtherMethod()
someCallbackMethod(data1, data2, (err, finalData) => {
if (err) {
return reject(err)
}
resolve(finalData)
})
} catch (e) {
reject(e)
}
})
}
这段代码又长又复杂,它使用了 Promises、Async/Await 和回调。我们在 Promise 中有一个异步函数,这是一个不好的地方,也不是一件好事。因为这是不可预期的,可能会导致许多隐藏的错误。它将分配额外的 Promise 对象,这些对象将不必要地浪费内存,并且您的垃圾收集器将花费更多时间清理它。
如果你想在 Promise 内部使用异步行为,你应该在外部方法中解决它们,或者使用 Async/Await 来链接内部的方法。我们可以这样重构代码:
const somePromiseMethod = (data1, data2) => {
return new Promise((resolve, reject) => {
someCallbackMethod(data1, data2, (err, finalData) => {
if (err) {
return reject(err)
}
resolve(finalData)
})
})
}
async function mainMethod() {
try {
const data1 = await someMethod()
const data2 = await someOtherMethod()
return somePromiseMethod(data1, data2)
} catch (e) {
console.error(e)
}
}
8、在Async函数中返回Promise
async function method() {
return new Promise((resolve, reject) => { ... })
}
这是不必要的,一个返回 Promise 的函数不需要关键字async
,相反,当函数是 async 时,你不需要return new Promise
在里面写。仅当您要在函数中使用await
时才使用async
关键字,并且该函数将在调用时返回一个 Promise。
function method() {
return new Promise((resolve, reject) => { ... })
}
或者
async function method() {
const data = await someAsyncMethod()
return data
}
9、将回调定义为异步函数
人们经常在意想不到的地方使用异步函数,例如回调。在下面的示例中,我们使用异步函数作为来自服务器的事件的回调:
server.on('connection', async stream => {
const user = await saveInDatabase()
console.log(`Someone with ID ${user.id} connected`)
})
这是一个反模式。无法在 EventEmitter 回调中等待结果,结果将丢失。此外,抛出的任何错误(例如无法连接到数据库)都不会得到处理,相反,您将得到unhandledRejection,并且没有办法处理它。
使用非 Promise Node.js API
Promises 是 ECMAScript 中的一种新实现,并非所有 Node.js API 都以这种方式工作。这就是为什么我们有一个 Promisify 方法可以帮助我们生成一个函数,该函数从一个与回调一起工作的函数返回一个 Promise:
const util = require('util')
const sleep = util.promisify(setTimeout)
sleep(1000)
.then(() => console.log('This was executed after one second'))
结论
编程语言中的每一个新特性都会让人兴奋但又急于尝试。但是没有人愿意花时间去理解功能;我们只是想使用它。这就是问题所在,Promises 也是如此。
如果您在使用 Promises 时遇到更大的问题并且需要一些分析方面的帮助,我们建议您使用Node Clinic – 一组可以帮助您诊断 Node.js 应用程序性能问题的工具。
转载自:https://juejin.cn/post/7241080533168652346