【问题标题】:How to unnest these Promises?如何取消这些 Promise?
【发布时间】:2018-12-24 21:46:25
【问题描述】:

背景

我有一个向服务器发出请求的函数。如果请求失败,我想: 1.记录错误 2.运行终端命令 2.1 记录命令失败或成功

为了实现这一点,我有以下代码:

const createRequest = ( { request, logger, terminal } ) => ( { endpoint, timeout } ) =>
    request.get( endpoint, { timeout } )
        .then( response =>
            logger.info( { event: "Heartbeat request succeeded.", status: response.status } )
        )
        .catch( err =>
            logger.error( { event: "Heartbeat request failed.", err } )
                .then( ( ) => terminal.runAsync( "pm2 restart myAPI" ) )
                .then( ( ) => logger.info( { event: "Restarted API." } ) )
                .catch( err => logger.error( { event: "Failed to restart API.",  err } ) )
        );

现在,有几点需要注意: - 日志是异步的(将信息发送到远程服务器) - 运行终端命令是异步的 - 发出请求(显然)是异步的

有问题?

我的问题是我的 catch 里面有一个 Promise,这意味着我有嵌套。 现在,我强烈反对 promise 的嵌套,所以我真的很想摆脱它,但我就是不知道怎么做。

问题

  1. 是否可以摆脱 catch 中的嵌套承诺?
  2. 如果有怎么办?

【问题讨论】:

  • 不使用 async / await 语法有什么原因吗?我
  • 你为什么反对嵌套承诺?
  • @k0pernikus 我更像是一个实用的人。 async / await 适用于命令式粉丝。
  • @evolutionxbox 因为承诺嵌套是一种反模式 (taoofcode.net/promise-anti-patterns)
  • @Flame_Phoenix 嵌套承诺不是反模式。一些计算需要以保证的顺序对 Promise 进行排序,但 Promise.all 并行运行所有 Promise。 – 我不确定你为什么认为async/await 语法是必要的。请参阅this example 和this one

标签: javascript functional-programming promise es6-promise


【解决方案1】:

有问题?

我的问题是我的 catch 里面有一个 Promise,这意味着我有嵌套。现在,我强烈反对 promise 的嵌套,所以我真的很想摆脱它,但我就是不知道怎么做。

– Flame_Phoenix

问题是你认为你有问题——或者你在 StackOverflow 上而不是 CodeReview 上发布了这个问题。 article you linked 显示了您对嵌套 Promise 的幼稚看法

你会得到一整套相互嵌套的承诺:

loadSomething().then(function(something) {
    loadAnotherthing().then(function(another) {
                    DoSomethingOnThem(something, another);
    });
});

你这样做的原因是因为你需要对两个 promise 的结果做一些事情,所以你不能把它们链接起来,因为 then() 只传递了前一个 return 的结果。

您这样做的真正原因是您不了解Promise.all() 方法。

– 代码猴子,http://taoofcode.net

不,Promise.all 只能有时替换嵌套的 Promise。一个简单的反例——这里,一个promise的值依赖于另一个,所以两个必须被排序

getAuthorByUsername (username) .then (a => getArticlesByAuthorId (a.id))

嵌套 promise 并不是总是必要的,而是将其称为“反模式”并鼓励人们在他们知道差异有害之前避免它,imo。


语句不起作用

other linked article 显示您可能再次被误导的地方

不要误会我的意思,async/await 并不是世界上万恶之源。在使用它几个月后,我实际上学会了喜欢它。因此,如果您对编写命令式代码感到满意,那么学习如何使用 async/await 来管理您的异步操作可能是一个不错的举措。

但是,如果您喜欢 Promise,并且喜欢学习并将越来越多的函数式编程原则应用到您的代码中,您可能希望完全跳过 async/await 代码,停止思考命令式并转向这种新旧范式。

——加布里埃尔·蒙特斯

只有这没有任何意义。如果您查看 JavaScript 中的所有命令式关键字,您会注意到它们都没有计算为值。为了说明我的意思,请考虑

let total = if (taxIncluded) { total } else { total + (total * tax) }
// SyntaxError: expected expression, got keyword 'if'

或者如果我们尝试在另一个表达式的中间使用if

makeUser (if (person.name.length === 0) { "anonymous" } else { person.name })
// SyntaxError: expected expression, got keyword 'if'

这是因为if 是一个语句,它永远不会计算出一个值——相反,它只能依赖副作用。

if (person.name.length === 0)
  makeUser ("anonymous") // <-- side effect
else
  makeUser (person.name) // <-- side effect

低于for 永远不会计算出一个值。相反,它依赖于副作用来计算sum

let sum = 0
let numbers = [ 1, 2, 3 ]
for (let n of numbers)
  sum = sum + n        // <-- side effect
console.log (sum)      // 6

do、while、switch 甚至 return 以及所有其他命令式关键字也是如此——它们都是语句,因此依赖于副作用计算值。

那么什么会计算出一个值呢? 表达式求值

1                          // => 1
5 + 5                      // => 10
person.name                // => "bobby"
person.name + person.name  // => "bobbybobby"
toUpper (person.name)      // => "BOBBY"
people .map (p => p.name)  // => [ "bobby", "alice" ]

async 和 await 不是陈述

您可以将异步函数分配给变量

const f = async x => ...

或者您可以将异步函数作为参数传递

someFunc (async x => ... )

即使 async 函数什么也不返回,async 仍然保证我们会收到一个 Promise 值

const f = async () => {}
f () .then (() => console.log ("done"))
// "done"

您可以await 一个值并将其分配给一个变量

const items = await getItems () // [ ... ]

或者你可以await另一个表达式中的值

items .concat (await getMoreItems ()) // [ ... ]

因为async/await 形成了表达式,所以它们可以与函数式风格一起使用。如果你想学习函数式风格并避免async 和await,那只是因为你被误导了。如果 async 和 await 只是命令式风格,那么这样的事情永远不可能发生

const asyncUnfold = async (f, initState) =>
  f ( async (value, nextState) => [ value, ...await asyncUnfold (f, nextState) ]
    , async () => []
    , initState
    )

实例

这是一个实际示例,我们有一个记录数据库,我们希望执行递归查找或其他操作...

const data =
  { 0 : [ 1, 2, 3 ]
  , 1 : [ 11, 12, 13 ]
  , 2 : [ 21, 22, 23 ]
  , 3 : [ 31, 32, 33 ]
  , 11 : [ 111, 112, 113 ]
  , 33 : [ 333 ]
  , 333 : [ 3333 ]
  }

一个异步函数Db.getChildren 位于您和您的数据之间。如何查询一个节点及其所有的后代?

const Empty =
  Symbol ()

const traverse = (id) =>
  asyncUnfold
    ( async (next, done, [ id = Empty, ...rest ]) =>
        id === Empty
          ? done ()
          : next (id, [ ...await Db.getChildren (id), ...rest ])
    , [ id ]
    )

traverse (0)
// => Promise [ 0, 1, 11, 111, 112, 113, 12, 13, 2, 21, 22, 23, 3, 31, 32, 33, 333, 3333 ]

用蒙特斯的话来说,是从“JavaScript 开发者的天堂”送来的纯程序。它是使用函数表达式编写的,错误会相应地冒出,我们甚至不必碰.then。

我们可以使用命令式风格编写相同的程序。或者我们也可以使用.then 来编写函数式风格。我们可以用各种方式编写它,我想这就是重点——感谢 async 和 await 形成表达式的能力,我们可以在各种风格中使用它们,包括函数式风格。

在下面的浏览器中运行整个程序

const asyncUnfold = async (f, init) =>
  f ( async (x, acc) => [ x, ...await asyncUnfold (f, acc) ]
    , async () => []
    , init
    )

const Db =
  { getChildren : (id) =>
      new Promise (r => setTimeout (r, 100, data [id] || []))
  }

const Empty =
  Symbol ()

const traverse = (id) =>
  asyncUnfold
    ( async (next, done, [ id = Empty, ...rest ]) =>
        id === Empty
          ? done ()
          : next (id, [ ...await Db.getChildren (id), ...rest ])
    , [ id ]
    )
    
const data =
  { 0 : [ 1, 2, 3 ]
  , 1 : [ 11, 12, 13 ]
  , 2 : [ 21, 22, 23 ]
  , 3 : [ 31, 32, 33 ]
  , 11 : [ 111, 112, 113 ]
  , 33 : [ 333 ]
  , 333 : [ 3333 ]
  }

traverse (0) .then (console.log, console.error)
// => Promise
// ~2 seconds later
// [ 0, 1, 11, 111, 112, 113, 12, 13, 2, 21, 22, 23, 3, 31, 32, 33, 333, 3333 ]

【讨论】:

  • 一个令人难以置信的回应!只希望我有更多这样的!
【解决方案2】:

我认为有两种方法。

选项 1

继续使用 Promise,但代码需要一些更改:

const createRequest = ({ request, logger, terminal }) => ({ endpoint, timeout }) =>
  request.get(endpoint, { timeout })
    .then(response =>
      logger.info({ event: "Heartbeat request succeeded.", status: response.status })
    )
    .catch(err => {
      // after a catch the chain is restored
      return logger.error({ event: "Heartbeat request failed.", err })
    })
    .then(() => terminal.runAsync("pm2 restart myAPI"))
    .then(() => logger.info({ event: "Restarted API." }))
    .catch(err => logger.error({ event: "Failed to restart API.", err }))

选项 2

使用等待/异步,将异步更改为同步。

【讨论】:

  • 如果请求成功会发生什么? (这是提示,您的代码不正确:P)。异步 / 等待是不可能的。
  • @evolutionxbox 何时用于谓词条件。这里的谓词是什么?我相信正确的功能是尝试/捕获,尽管它可能具有讽刺意味。还是不错的建议!
  • 您的日志记录会嵌套 - 即使它是完全同步的,您也会嵌套......
猜你喜欢
  • 2017-08-02
  • 1970-01-01
  • 1970-01-01
  • 2022-11-02
  • 2014-10-10
  • 2022-01-08
  • 2017-02-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多