注意那些 Promise 反模式
JavaScript 作为天生的客户端脚本,编写异步逻辑有着天然的优势, 比如嵌套函数(很自然的闭包机制),事件模型(多数宿主都有提供),回调函数(函数是一等公民)。 Promise 用来更好地组织异步代码, 与其他设计模式类似,未能理解其设计意图之前容易误用和滥用。本文列举其中常见的反模式。
读者有更精彩的反模式例子,欢迎评论或者邮件。本文中的代码仅用于示例,未必存在这样的接口。
莫须有的嵌套
Promise 模式意在 Flatten 异步代码(参考 Callback Hell), 所以在你嵌套很深时就要反思了。
// 反模式
Posts
.find({ author: 'harttle' })
.then(posts => {
return posts.populate('comments')
.then(posts => {
render(posts)
.then(html => res.end(html))
.catch(err => res.status(500).end(err.message))
})
.catch(err => res.status(500).end(err.message))
})
.catch(err => res.status(500).end(err.message))
// 正常的写法
Posts
.find({ author: 'harttle' })
.then(posts => posts.populate('comments'))
.then(posts => render(posts))
.then(html => res.end(html))
.catch(err => res.status(500).end(err.message))
一般情况下 Promise 都不需要嵌套在另一个 Promise 中。
除非你需要多个 Promise 的解析值,这时可以使用 .all()
API:
Promise
.all([getPosts(), getComments()])
.spread((posts, comments) => {
// 处理文章和评论
})
Promisify 一个 Promise
用 Promise 封装一个已经是 Promise 的异步接口。比如:
// 反模式
function getPosts() {
return new Promise((resolve, reject) => {
doGetPosts()
.then(posts => resolve(posts))
.catch(err => reject(err))
})
}
// 正常的写法
function getPosts() {
return doGetPosts()
}
这一写法虽然看起来不可思议,但写起来确实容易掉进去。
尤其是在一个 Promise 实现的系统(比如 Bluebird)
中返回另一个 Promise 实现(比如 jQuery)的实例时。这时可以简单地用 .resolve()
来转换:
var Promise = require('bluebird')
function getPosts() {
return Promise.resolve($.get('https://harttle.land'))
}
随意吞掉错误
Promise 托管异步代码返回的同时也托管了异步错误。 不添加错误处理会吞掉错误,丢掉一个 Promise 的返回值也会直接吞掉错误。 这里要格外小心,消失的错误可能会要你一夜来找到它。比如:
// 反模式
function commentToPost(postId) {
Posts
.findOne({ id: postId })
.then(post => {
// 如果这里出错了,任何人都不知道
post.addComment("Great idea! but I dont't care", 'Alice')
})
.then(() => res.end('ok'))
// 这里收不到 addComment 的错误
.catch(err => res.status(500).end('wow'))
}
// 正常的写法
function commentToPost(postId) {
Posts
.findOne({ id: postId })
.then(post => post.addComment("Great idea! but I dont't care", 'Alice'))
.then(() => res.end('ok'))
.catch(err => res.status(500).end('wow'))
}
幸好像 Eslint 这样的工具已经可以在你编写代码的同时检查出丢掉的 Promise 返回值。 对于 Eslint,Harttle 有 一份很好的 Vim 配置。
Promise 数组
在 for 循环中创建 Promise 会非常难以理解,许多 Promise 实现都为此提供了
.map()
, .reduce()
等 API,好好利用它们!
// 反模式
function getSites(authors) {
return new Promise((resolve, reject) => {
var sites = []
for (var i=0; i<authors.length; i++) {
Site
.findOne({ author: authors[i] })
.then(site => {
sites.push(site)
if (sites.length === authors.length) {
resolve(sites)
}
})
.catch(err => reject(err))
}
})
}
// 正常的写法
function getSites(authors) {
return Promise
.resolve(authors)
.map(author => Site.findOne({ author }))
.catch(err => reject(err))
}
此外,有些 API 还提供了更精细的控制,比如 .any()
, .some()
, .all()
等。
为方便表达文中使用了大量的 Arrow Function,但 ES6 中更推荐使用 await/async 模式。
本文采用 知识共享署名 4.0 国际许可协议(CC-BY 4.0)进行许可,转载注明来源即可: https://harttle.land/2017/06/27/promise-anti-patterns.html。如有疏漏、谬误、侵权请通过评论或 邮件 指出。