Async: Trate funções sem um retorno de chamada como promessas

Criado em 9 jul. 2019  ·  12Comentários  ·  Fonte: caolan/async

O comportamento atual é muito surpreendente:

await async.parallel({foo: async()=>Promise.all([1,2])})
// { foo: [ 1, 2 ] }
await async.parallel({foo: ()=>Promise.all([1,2])})
// waits forever because non-async function expects a ballback

Porque para funções curtas, é tentador escrever apenas foo: () => doSomething() . É muito fácil esquecer a palavra-chave async aqui

Mas atualmente isso não funciona, ele precisa ser escrito como foo: async () => doSomething()

Poderíamos antes verificar se um retorno de chamada foi passado ou não, em vez de verificar se a função é uma AsyncFunction?

wont fix

Comentários muito úteis

Mesmo com este problema encerrado, a partir do problema vinculado acima, gostaria de enfatizar novamente as declarações
O comportamento atual me confundiu muito e, embora eu (meio que) entenda as limitações técnicas discutidas aqui, acredito que a documentação simplesmente não está de acordo com o comportamento atual.
Por exemplo, mapLimit diz "Retorna: uma promessa, se nenhum retorno de chamada for passado" - comecei com a versão de retorno de chamada e, basicamente, deixei de lado o retorno de chamada e fiquei surpreso por não receber a promessa (porque eu não tinha a palavra-chave 'async').
além disso, a instrução 'callback' na função iteratee não é necessária, em vez disso, você precisa 'retornar' um valor.
Portanto, ou o comportamento deve ser alterado (preferencial) ou, pelo menos, a documentação e os exemplos devem ser alinhados ao meu ponto de vista para evitar confusão.

Todos 12 comentários

Não temos como detectar com segurança se uma função retorna uma promessa ou não, e se precisamos passar um retorno de chamada para ela ou não com antecedência. ( Function.length ou análise sintática Function.toString() não é bom o suficiente) É disso que trata asyncify - converter funções de retorno de promessa em funções que recebem um retorno de chamada. Uma vez que podemos detectar funções async por terem um tipo diferente, podemos automaticamente asyncify them (que é como isso funciona internamente). Mas, para funções comuns que retornam uma promessa, não temos como saber antes que o Async as chame.

@aearly hmm certo, então talvez algo mais simples como detectar o retorno de chamada no nível superior?

async function parallel(o, cb) {
  if (!cb) { // promise behavior
    return Object.fromEntries(
      await Promise.all(Object.entries(o).map(async ([k, v]) => [k, await v()]))
    )
  }
  // else do the callback behavior
}

Já fizemos algo assim - retorne uma promessa se você omitir o retorno de chamada para async.parallel . Não presumimos que as funções que você passa para ele retornam promessas se você omitir o retorno de chamada, porque não temos como assumir a intenção do usuário. Talvez eu esteja entendendo mal o que você quer dizer.

Ok, vamos ser concretos: https://repl.it/@caub/async -async

// npm.im/async expects all inner functions to be async,
// even when using no outer callback (as 2nd arg of async.parallel)
console.log(
  await async.parallel({
    foo: async () => delay(1000).then(() => 1),
    bar: async (rs) => {
      return 2
    },
  })
);

// Doesn't work without those async,
// but I'd like this to resolve just like above instead of freezing
console.log(
  await async.parallel({
    foo: () => delay(1000).then(() => 1),
    bar: () => {
      return 2
    },
  })
);

Eu acho que, em uma próxima versão principal futura, poderia ser interessante oferecer suporte apenas para retorno de chamada de uma forma semelhante a esta:

await Promise.all([
  { then(resolve){ setTimeout(resolve, 50, 1) } }, // callback way
  2,
  delay(45).then(())=>3)
])
// [1, 2, 3]

removendo a necessidade de async palavras-chave desnecessárias nas funções internas

A vantagem é mais clareza. Eu entendo seu ponto de não assumir a intenção do usuário, mas você ainda está assumindo que as pessoas não esquecerão a palavra-chave async quando quiserem usar a resolução de promessa

Isso quebraria a compatibilidade com versões anteriores de uma forma muito grande - por exemplo, você não poderia fazer coisas como Async.map(fileNames, fs.readFile) mais.

Você poderia explicar onde iria quebrar?

O que está quebrado atualmente é:

await async.map(['server.js', 'package.json'], fs.readFile) // works
await async.map(['server.js', 'package.json'], fs.promises.readFile) // works
await async.map(['server.js', 'package.json'], s => fs.promises.readFile(s)) // doesn't work
await async.map(['server.js', 'package.json'], async s => fs.promises.readFile(s)) // works

A ideia é consertar o terceiro

Eu tenho esses problemas constantemente, por exemplo, eu escrevi:

  // ...
  doc = await async.map(items, item => item.toDoc(currentUser, options));

em algum lugar, e novamente, tempo limite porque estava faltando um async item => item.toDoc..

Se eu estiver sozinho nesse caso, não vale a pena, mas se houver mais pessoas nessa situação, acho que vale a pena considerar a mudança proposta

Este exemplo com async.map também é muito representativo, pois você pode ver meu ponto de como

  doc = await Promise.all(items.map(item => item.toDoc(currentUser, options)));

funciona sem a necessidade de async item => item.toDoc..

Não há como consertar o terceiro com segurança, isso já foi muito discutido no passado. Não podemos verificar o valor de retorno de uma função e, em seguida, retroativamente, não passar um retorno de chamada para a função. A palavra-chave async é o melhor que existe.

@caub Plugue sem vergonha aqui, mas acho que asyncp tem a capacidade de lidar com o que você está procurando aqui.

Mesmo com este problema encerrado, a partir do problema vinculado acima, gostaria de enfatizar novamente as declarações
O comportamento atual me confundiu muito e, embora eu (meio que) entenda as limitações técnicas discutidas aqui, acredito que a documentação simplesmente não está de acordo com o comportamento atual.
Por exemplo, mapLimit diz "Retorna: uma promessa, se nenhum retorno de chamada for passado" - comecei com a versão de retorno de chamada e, basicamente, deixei de lado o retorno de chamada e fiquei surpreso por não receber a promessa (porque eu não tinha a palavra-chave 'async').
além disso, a instrução 'callback' na função iteratee não é necessária, em vez disso, você precisa 'retornar' um valor.
Portanto, ou o comportamento deve ser alterado (preferencial) ou, pelo menos, a documentação e os exemplos devem ser alinhados ao meu ponto de vista para evitar confusão.

@caub Este é o motivo
https://github.com/caolan/async/blob/8aecf108b3922bc5211036706a0f6f75e02bd42b/lib/internal/wrapAsync.js#L3 : L5

function isAsync(fn) {
    return fn[Symbol.toStringTag] === 'AsyncFunction';
}

Dessa forma, é mais fácil verificar se a função retorna uma promessa sem realmente chamá-la. Mas ainda concordo com você que isso é contra as convenções async / await. Como adicionar uma palavra-chave async sem uma await interna é considerado uma prática ruim na maioria dos casos, na verdade.

de https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function

O corpo de uma função async pode ser considerado dividido por zero ou mais expressões de espera. O código de nível superior, até e incluindo a primeira expressão de espera (se houver), é executado de forma síncrona.

... e assim será inútil e o pior irá adicionar alguma sobrecarga de desempenho se o código for transpilado por babel, por exemplo, pois irá adicionar mais código e etapas apenas por causa da existência da palavra-chave async

Por exemplo, o seguinte:

async function foo() {
   return 1
}

...é equivalente a:

function foo() {
   return Promise.resolve(1)
}

mas com esta biblioteca,

async.mapLimit([1], 1, async v => v)

e

async.mapLimit([1], 1, v => Promise.resolve(v)))

não são equivalentes!

Esta página foi útil?
0 / 5 - 0 avaliações