Promise 链非常棒

2025-06-08

Promise 链非常棒

哦,你来这里是为了承诺吗?是的,我们马上就讲,不过首先我先给你介绍一下我的一个朋友,叫 Trace。

const trace = tag => x =>
  console.log(tag, x) || x;
Enter fullscreen mode Exit fullscreen mode

几年前,我们在@drBoolean的一次聚会上认识,一拍即合。我发现我们有很多共同点:我们都很有自我认同感,但只要有必要,我们也不怕做出一些小改变。这孩子做的咖喱也很棒。

trace :: Show t => t -> a -> a
Enter fullscreen mode Exit fullscreen mode

你看,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
Enter fullscreen mode Exit fullscreen mode
使用Array#Map 映射数组
const handleAsJson = resp => resp.json()

fetch(`/users`)
  .then(handleAsJson)
  .then(trace('all users: '))

// all users: [{ id: 1, isAdmin: false }, { id: 2, isAdmin: true }]
Enter fullscreen mode Exit fullscreen mode
使用Promise#then 映射 Promise

Trace 乍一看可能微不足道,甚至有些轻浮。但其简洁性恰恰体现了它的强大。它是一种简单、原子化、一物多用的功能,可以轻松地组合成越来越庞大的计算。

不管怎样,我在这里有点跑题了。

于是有一天,我和特蕾丝决定举办一个晚宴。我们把这件事分解成一个简短的待办事项清单。

  1. 拟定宾客名单
  2. 发出邀请
  3. 订购配料
  4. 烹制主菜
  5. 供应晚餐
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
}
Enter fullscreen mode Exit fullscreen mode

对我来说,这种从上到下、从左到右的流程既可读又美观。它每次只需要我关注一件事,那就是每次then调用时传递的函数。

但这种流程会与 VS-Code 的“意见灯泡真相™️”相冲突

屏幕截图显示 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)
}
Enter fullscreen mode Exit fullscreen mode

需要多少状态、多少声明、多少心理执行才能安抚雷德蒙德的风格霸主?

通过关闭进行分配

假设你需要跟踪用户,以便根据他们的饮食需求为他们提供个性化服务。我们可以使用闭包来实现。现在不是讨论闭包令人困惑的技术定义的时候,我们先假设一个函数可以访问它自己的参数。

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
}
Enter fullscreen mode Exit fullscreen mode

警告:在这个设计好的例子中,我使用了闭包来说明这一点,但在实际应用中,我可能会使用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
PREV
您应该使用 esm Markdown 标题应该具有用于​​链接的 ID #682
NEXT
一起构建 Web 组件!第二部分:Gluonjs 的 Polyfill