Javascript promete curiosidade

96

Quando eu chamo essa promessa, a saída não corresponde à sequência de chamadas de função. O .thenvem antes do .catch, embora a promessa com .thentenha sido chamada depois. Qual é o motivo disso?

const verifier = (a, b) =>
  new Promise((resolve, reject) => (a > b ? resolve(true) : reject(false)));

verifier(3, 4)
  .then((response) => console.log("response: ", response))
  .catch((error) => console.log("error: ", error));

verifier(5, 4)
  .then((response) => console.log("response: ", response))
  .catch((error) => console.log("error: ", error));

resultado

node promises.js
response: true
error: false
Gustavo Alves
fonte
34
Você nunca deve confiar em intervalos entre cadeias independentes de promessas.
Bergi

Respostas:

136

Essa é uma pergunta legal para se chegar ao fundo.

Quando você faz isso:

verifier(3,4).then(...)

que retorna uma nova promessa que requer outro ciclo de volta ao loop de eventos antes que a promessa rejeitada possa executar o .catch()manipulador a seguir. Esse ciclo extra dá a próxima sequência:

verifier(5,4).then(...)

a chance de executar seu .then()manipulador antes da linha anterior .catch()porque ele já estava na fila antes que o .catch()manipulador da primeira entre na fila e os itens sejam executados da fila em ordem FIFO.


Observe que, se você usar o .then(f1, f2)formulário no lugar do .then().catch(), ele será executado quando você espera, porque não há promessa adicional e, portanto, nenhuma marca adicional envolvida:

const verifier = (a, b) =>
  new Promise((resolve, reject) => (a > b ? resolve(true) : reject(false)));

verifier(3, 4)
  .then((response) => console.log("response (3,4): ", response),
        (error) => console.log("error (3,4): ", error)
  );

verifier(5, 4)
  .then((response) => console.log("response (5,4): ", response))
  .catch((error) => console.log("error (5,4): ", error));

Observe, também rotulei todas as mensagens para que você possa ver de qual verifier()chamada elas vêm, o que torna muito mais fácil ler a saída.


ES6 Especificação sobre pedido de retorno de chamada de promessa e explicação mais detalhada

A especificação ES6 nos diz que os "trabalhos" de promessa (já que chama um retorno de chamada de um .then()ou .catch()) são executados em ordem FIFO com base em quando são inseridos na fila de trabalhos. Ele não nomeia especificamente FIFO, mas especifica que novos trabalhos são inseridos no final da fila e os trabalhos são executados a partir do início da fila. Isso implementa o pedido FIFO.

PerformPromiseThen (que executa o retorno de chamada de .then()) levará a EnqueueJob, que é como o manipulador de resolução ou rejeição é programado para ser executado de fato. EnqueueJob especifica que o trabalho pendente é adicionado no final da fila de trabalhos. Em seguida, a operação NextJob puxa o item da frente da fila. Isso garante ordem FIFO em trabalhos de serviço da fila de trabalhos Promise.

Portanto, no exemplo da pergunta original, obtemos os retornos de chamada para a verifier(3,4)promessa e a verifier(5,4)promessa inserida na fila de tarefas na ordem em que foram executadas porque ambas as promessas originais foram cumpridas. Então, quando o intérprete volta ao loop de eventos, ele primeiro pega o verifier(3,4)trabalho. Essa promessa foi rejeitada e não há retorno de chamada para isso no verifier(3,4).then(...). Portanto, o que ele faz é rejeitar a promessa que verifier(3,4).then(...)retornou e que faz com que o verifier(3,4).then(...).catch(...)manipulador seja inserido no jobQueue.

Em seguida, ele volta ao loop de eventos e o próximo trabalho que extrai de jobQueue é o verifier(5, 4)trabalho. Isso tem uma promessa resolvida e um manipulador de resolução, portanto, ele chama esse manipulador. Isso faz com que a response (5,4):saída seja exibida.

Em seguida, ele volta para o loop de eventos e o próximo trabalho que puxa do jobQueue é o verifier(3,4).then(...).catch(...)trabalho onde é executado e isso faz com que a error (3,4)saída seja mostrada.

É porque .catch()na 1ª cadeia há um nível de promessa mais profundo em sua cadeia do que .then()na 2ª cadeia que causa a ordem que você relatou. E é porque as cadeias de promessa são percorridas de um nível para o próximo por meio da fila de tarefas na ordem FIFO, não de forma síncrona.


Recomendação geral sobre confiar neste nível de detalhe de programação

Para sua informação, em geral, tento escrever um código que não dependa desse nível de conhecimento detalhado de temporização. Embora seja curioso e ocasionalmente útil de entender, é um código frágil, pois uma mudança simples e aparentemente inócua no código pode levar a uma mudança no tempo relativo. Portanto, se o tempo for crítico entre duas cadeias como essa, prefiro escrever o código de uma forma que force o tempo da maneira que desejo, em vez de confiar nesse nível de compreensão detalhada.

jfriend00
fonte
Para ser mais específico, esse comportamento exato não está documentado em nenhuma parte da especificação de promessas, o que torna isso um detalhe de implementação. Você pode obter comportamentos diferentes entre os intérpretes (por exemplo, Node.js vs Edge vs Firefox) ou entre as versões de intérpretes (por exemplo, Node 12 vs Node 14). A especificação apenas diz que as promessas são processadas de forma assíncrona para evitar o código zalgo (que IMHO foi mal orientado, BTW, porque foi motivado por pessoas que estavam fazendo perguntas como esta querendo depender do tempo de código potencialmente assíncrono)
slebetman
@slebetman - Não está documentado que retornos de chamada de promessa de promessas separadas são chamados de FIFO com base em quando foram inseridos na fila e não podem ser executados até o próximo tique? Parece que a ordenação FIFO é tudo o que é necessário aqui porque .then()tem que retornar uma nova promessa que ela mesma tem que resolver / rejeitar de forma assíncrona em um tick futuro que é o que leva a essa ordem. Você conhece alguma implementação que não usa ordenação FIFO de callbacks concorrentes?
jfriend00
3
@slebetman Promises / A + não especifica isso. ES6 especifica isso. (ES11 mudou o comportamento de await, no entanto).
Bergi
Da especificação ES6 na ordem de enfileiramento. PerformPromiseThenlevará a EnqueueJobque é como o manipulador de resolução ou rejeição é programado para ser chamado. EnqueueJob especifica que o trabalho pendente é adicionado no final da fila de trabalhos. Em seguida, a operação NextJob puxa o item da frente da fila. Isso garante a ordem FIFO na fila de trabalhos do Promise.
jfriend00
@Bergi Qual é essa mudança awaitno ES11? Um link é suficiente. Obrigado!!
Pedro A
49

Promise.resolve()
  .then(() => console.log('a1'))
  .then(() => console.log('a2'))
  .then(() => console.log('a3'))
Promise.resolve()
  .then(() => console.log('b1'))
  .then(() => console.log('b2'))
  .then(() => console.log('b3'))

Em vez da saída a1, a2, a3, b1, b2, b3, você verá a1, b1, a2, b2, a3, b3 por causa do mesmo motivo - a cada um retorna uma promessa e vai para o final do loop de eventos fila. Então podemos ver essa "corrida de promessas". O mesmo é quando há algumas promessas aninhadas.

Tarukami
fonte