Promise 链非常棒
哦,你来这里是为了承诺吗?是的,我们马上就讲,不过首先我先给你介绍一下我的一个朋友,叫 Trace。
const trace = tag => x =>
console.log(tag, x) || x;
几年前,我们在@drBoolean的一次聚会上认识,一拍即合。我发现我们有很多共同点:我们都很有自我认同感,但只要有必要,我们也不怕做出一些小改变。这孩子做的咖喱也很棒。
trace :: Show t => t -> a -> a
你看,Trace 的特色就是,他不在乎你把他放哪儿,他只想做自己想做的事。算是随波逐流吧,我保证!
['a', 'b', 'c']
.map(trace('what do we have here...'))
// what do we have here ... a
// what do we have here ... b
// what do we have here ... c
const handleAsJson = resp => resp.json()
fetch(`/users`)
.then(handleAsJson)
.then(trace('all users: '))
// all users: [{ id: 1, isAdmin: false }, { id: 2, isAdmin: true }]
Trace 乍一看可能微不足道,甚至有些轻浮。但其简洁性恰恰体现了它的强大。它是一种简单、原子化、一物多用的功能,可以轻松地组合成越来越庞大的计算。
不管怎样,我在这里有点跑题了。
于是有一天,我和特蕾丝决定举办一个晚宴。我们把这件事分解成一个简短的待办事项清单。
- 拟定宾客名单
- 发出邀请
- 订购配料
- 烹制主菜
- 供应晚餐
const handleAsJson = resp => resp.json()
const map = f => xs => xs.map(f)
const all = Promise.all.bind(Promise)
const fetchGuests = () => fetch('/friends')
const fetchShoppingList = () => fetch('/shopping-list')
const order = item => fetch(`https://groceries.for.you/order/${item}`)
const invite = body => to =>
fetch(`/sendmail?to="${encodeURIComponent(to)}`, { method: 'POST', body })
const getEmail = ({ email }) => email
const cook = xs => xs.reduce(fricassee, 'a delicious ')
const serve = dish => alert(`${dish} is served!`)
const fricassee = (a, x, i, {length}) =>
`${a}-${x}${i === length - 1 ? ' fricassee' : ''}`
function party() {
return fetchGuests()
.then(handleAsJson) // Promise<[person]>
.then(map(getEmail)) // Promise<[string]>
.then(map(invite)) // Promise<[Response]>
.then(all) // Promise<[invitation]>
.then(fetchShoppingList) // discard previous result, as `fetchShoppingList` takes no arguments.
.then(handleAsJson) // Promise<[item]>
.then(map(order)) // Promise<[Promise<order>]>
.then(all) // Promise<[order]>
.then(cook) // Promise<Fricasee>
.then(serve) // et voila
}
对我来说,这种从上到下、从左到右的流程既可读又美观。它每次只需要我关注一件事,那就是每次then
调用时传递的函数。
但这种流程会与 VS-Code 的“意见灯泡真相™️”相冲突
考虑一下替代方案:
async function party() {
const guestsResponse = await fetchGuests()
const guests = await guestsResponse.json()
const emails = guests.map(getEmail)
const inviteResponses = emails.map(invite)
const listResponse = fetchShoppingList()
const list = listResponse.json()
const orderPromises = list.map(order)
const orderResponses = Promise.all(orderPromises)
const order = orderResponses.map(handleAsJson)
const dish = cook(order)
return serve(dish)
}
需要多少状态、多少声明、多少心理执行才能安抚雷德蒙德的风格霸主?
通过关闭进行分配
假设你需要跟踪用户,以便根据他们的饮食需求为他们提供个性化服务。我们可以使用闭包来实现。现在不是讨论闭包令人困惑的技术定义的时候,我们先假设一个函数可以访问它自己的参数。
const all = Promise.all.bind(Promise)
const constant = x => () => x
const not = p => x => !p(x)
const fanout = (f, g) => x => [f(x), g(x)]
const merge = f => ([x, y]) => f(x, y)
const bimap = (f, g) => ([xs, ys]) => [xs.map(f), ys.map(g)]
const serve = dish => guest => alert(`${guest} has been served ${dish}!`)
function party() {
return fetchShoppingList()
.then(handleAsJson)
.then(map(order))
.then(cook)
.then(dish => orderDietDishes() // no closing `)`, so dish stays in closure
.then(handleAsJson)
.then(dietDish => fetchGuests() // no closing `)`, so dietDish stays in closure
.then(handleAsJson)
.then(users => Promise.resolve(users)
.then(map(getEmail))
.then(map(invite))
.then(all)
.then(constant(users)))
.then(fanout(filter(hasDiet), filter(not(hasDiet))))
.then(merge(bimap(serve(dietDish), serve(dish)))))) // end closures from above
}
警告:在这个设计好的例子中,我使用了闭包来说明这一点,但在实际应用中,我可能会使用Crock中的数据类型而不是数组来进行扇出和合并,或者我可能会传递POJO来保存状态。我甚至可能会使用await
并赋值给,但我不会在调用处解开每个.单个. promise . ,const
从而把孩子和洗澡水一起倒掉。
总结
传递命名良好、简单易用、可组合的一级函数,会使代码读起来像散文一样流畅。像这样隔离计算阶段,可以将读者的认知负担转移到函数实现上,从而使你的程序更具可读性,也更易于维护。
诸如扇出元组和与二元函数合并之类的技术非常适合执行“并行”计算或将累积状态传递给纯函数。异步函数也有其用途,尤其是在闭包数量难以管理的情况下,但它们不应该取代每一次.then
调用。
答应我!
所以,只要你以最实用的方式使用 Promise 链,它就能让你的代码更具可读性,并为更优秀的软件做出贡献。下次有机会,不妨对那个小灯泡说“不用了,谢谢”——在你的应用中编写一个 Promise 链,享受自文档化、模块化的代码吧。
致谢与勘误
之前的版本演示了如何以Promise.all
一等成绩通过,即urls.map(fetch).then(Promise.all)
感谢@coagmano指出,Promise.all
如果您打算以一等成绩通过,则必须进行绑定。此处的代码片段已更新。
用户@kosich指出了第二个例子中的一个拼写错误(见评论),该错误现已得到纠正。
鏂囩珷鏉ユ簮锛�https://dev.to/bennypowers/promise-chains-are-kinda-awesome-273o