Por que a configuração da propriedade CSS usando Promise.then realmente não acontece no bloco then?

11

Tente executar o seguinte snippet e clique na caixa.

const box = document.querySelector('.box')
box.addEventListener('click', e => {
  if (!box.style.transform) {
    box.style.transform = 'translateX(100px)'
    new Promise(resolve => {
      setTimeout(() => {
        box.style.transition = 'none'
        box.style.transform = ''
        resolve('Transition complete')
      }, 2000)
    }).then(() => {
      box.style.transition = ''
    })
  }
})
.box {
  width: 100px;
  height: 100px;
  border-radius: 5px;
  background-color: #121212;
  transition: all 2s ease;
}
<div class = "box"></div>

O que eu espero que aconteça:

  • Clique acontece
  • A caixa começa a traduzir horizontalmente em 100px (esta ação leva dois segundos)
  • Ao clicar, um novo Promisetambém é criado. Por dentro Promise, uma setTimeoutfunção é definida para 2 segundos
  • Depois que a ação é concluída (dois segundos se passaram), setTimeoutexecuta sua função de retorno de chamada e define transitioncomo nenhum. Depois de fazer isso, setTimeouttambém reverte transformpara o valor original, fazendo com que a caixa apareça no local original.
  • A caixa aparece no local original sem nenhum problema de efeito de transição aqui
  • Depois de concluir tudo isso, defina o transitionvalor da caixa de volta ao seu valor original

No entanto, como pode ser visto, o transitionvalor não parece estar noneem execução. Eu sei que existem outros métodos para alcançar o que foi dito acima, por exemplo, usando o quadro-chave e transitionend, mas por que isso acontece? Eu explicitamente defini o valor de transitionvolta ao seu valor original somente após o término do setTimeoutretorno de chamada, resolvendo a Promessa.

EDITAR

Conforme solicitação, aqui está um gif do código que exibe o comportamento problemático: Problema

Richard
fonte
Em qual navegador você está vendo isso? No Chrome, estou vendo o que se destina.
Terry
@Terry Firefox 73.0 (64 bits) para Windows.
Richard
Você pode anexar um gif à sua pergunta que ilustra o problema? Tanto quanto eu poderia dizer, também está se tornando / se comportando conforme o esperado no Firefox.
Terry
Quando a Promessa é resolvida, a transição original é restaurada, mas neste momento a caixa ainda é transformada. Portanto, ele faz a transição de volta. Você precisa aguardar pelo menos mais um quadro antes de redefinir a transição para o valor original: jsfiddle.net/khrismuc/3mjwtack
Chris G
Quando executado várias vezes, consegui reproduzir o problema uma vez. A execução da transição depende do que o computador está fazendo em segundo plano. Adicionar mais tempo ao atraso antes de resolver a promessa pode ajudar.
Teemu

Respostas:

4

O loop de eventos agrupa as alterações de estilo. Se você alterar o estilo de um elemento em uma linha, o navegador não mostrará essa alteração imediatamente; esperará até o próximo quadro de animação. É por isso que, por exemplo

elm.style.width = '10px';
elm.style.width = '100px';

não resulta em cintilação; o navegador se preocupa apenas com os valores de estilo definidos após a conclusão de todo o Javascript.

A renderização ocorre após a conclusão de todo o Javascript, incluindo microtasks . O .thende uma promessa ocorre em uma microtask (que será executada efetivamente assim que todos os outros Javascript forem concluídos, mas antes que qualquer outra coisa - como renderização - tenha a chance de ser executada).

O que você está fazendo é definir a transitionpropriedade ''no microtask, antes que o navegador comece a renderizar a alteração causada por style.transform = ''.

Se você redefinir a transição para a sequência vazia depois de um requestAnimationFrame(que será executado logo antes da próxima repintura) e depois de um setTimeout(que será executado logo após a próxima repintura), funcionará conforme o esperado:

const box = document.querySelector('.box')
box.addEventListener('click', e => {
  if (!box.style.transform) {
    box.style.transform = 'translateX(100px)'
    setTimeout(() => {
      box.style.transition = 'none'
      box.style.transform = ''
      // resolve('Transition complete')
      requestAnimationFrame(() => {
        setTimeout(() => {
          box.style.transition = ''
        });
      });
    }, 2000)
  }
})
.box {
  width: 100px;
  height: 100px;
  border-radius: 5px;
  background-color: #121212;
  transition: all 2s ease;
}
<div class="box"></div>

CertainPerformance
fonte
Esta é a resposta que eu estava procurando. Obrigado. Você poderia, talvez, fornecer um link que descreva esses mecanismos de microtask e repintar ?
Richard
Parece um bom resumo: javascript.info/event-loop
CertainPerformance
2
Isso não é exatamente verdade não. O loop de eventos não diz nada sobre mudanças de estilo. A maioria dos navegadores tenta aguardar o próximo quadro de pintura quando puder, mas é isso. mais informações ;-)
Kaiido
3

Você está enfrentando uma variação do transição não funciona se o elemento iniciar um problema oculto , mas diretamente natransitionpropriedade

Você pode consultar esta resposta para entender como o CSSOM e o DOM estão vinculados para o processo de "redesenho".
Basicamente, os navegadores geralmente esperam até o próximo quadro de pintura para recalcular todas as novas posições da caixa e, assim, aplicar regras de CSS ao CSSOM.

Portanto, no manipulador Promise, quando você redefine o transitionpara "", o transform: ""ainda não foi calculado ainda. Quando for calculado, o transitionjá terá sido redefinido ""e o CSSOM acionará a transição para a atualização da transformação.

No entanto, podemos forçar o navegador a acionar um "reflow" e, portanto, podemos fazer com que recalcule a posição do seu elemento, antes de redefinir a transição para"" .

O que torna o uso da promessa bastante desnecessário:

const box = document.querySelector('.box')
box.addEventListener('click', e => {
  if (!box.style.transform) {
    box.style.transform = 'translateX(100px)'
    setTimeout(() => {
      box.style.transition = 'none'
      box.style.transform = ''
      box.offsetWidth; // this triggers a reflow
      // even synchronously
      box.style.transition = ''
    }, 2000)
  }
})
.box {
  width: 100px;
  height: 100px;
  border-radius: 5px;
  background-color: #121212;
  transition: all 2s ease;
}
<div class = "box"></div>


E para obter uma explicação sobre as micro-tarefas, como Promise.resolve()ou MutationEvents , ou queueMicrotask(), você precisa entender que elas serão executadas assim que a tarefa atual for concluída, sétima etapa do modelo de processamento do loop de eventos , antes das etapas de renderização .
Portanto, no seu caso, é muito parecido com se fosse executado de forma síncrona.

A propósito, cuidado com as micro-tarefas podem ser tão bloqueadoras quanto um loop while:

// this will freeze your page just like a while(1) loop
const makeProm = ()=> Promise.resolve().then( makeProm );
Kaiido
fonte
Sim, exatamente, mas responder ao transitionendevento evitaria a necessidade de codificar um tempo limite para coincidir com o final da transição. A transiçãoToPromise.js promisificará a transição, permitindo que você escreva transitionToPromise(box, 'transform', 'translateX(100px)').then(() => /* four lines as per answer above */).
Roamer-1888
Duas vantagens: (1) a duração da transição pode ser modificada em CSS sem a necessidade de modificar o javascript; (2) transitToPromise.js é reutilizável. BTW, eu tentei e funciona bem.
Roamer-1888
@ Roamer-1888 sim, você poderia, embora eu pessoalmente adicionasse uma verificação (evt)=>if(evt.propertyName === "transform"){ ...para evitar falsos positivos e realmente não goste de prometer tais eventos, porque você nunca sabe se será acionado (pense em um caso como someAncestor.hide() quando a transição ocorre , sua promessa nunca fogo e sua transição vai ficar preso desativado Então é realmente até OP para determinar o que é melhor para eles, mas, pessoalmente, e por experiência, eu agora preferem o tempo limite de eventos transitionend..
Kaiido
11
Prós e contras, eu acho. Em qualquer caso, com ou sem promisificação, essa resposta é muito mais limpa do que uma que envolve dois setTimeouts e um requestAnimationFrame.
Roamer-1888
Ainda tenho uma dúvida. Você disse que requestAnimationFrame()será acionado pouco antes da repintagem do próximo navegador. Você também mencionou que os navegadores geralmente aguardam até o próximo quadro de pintura para recalcular todas as novas posições da caixa . No entanto, você ainda precisava disparar manualmente um reflow forçado (sua resposta no primeiro link). Por conseguinte, chego à conclusão de que, mesmo quando requestAnimationFrame()acontece imediatamente antes da repintura, o navegador ainda não calculou o estilo computado mais recente; portanto, a necessidade de forçar manualmente um recálculo de estilos. Corrigir?
Richard
0

Aprecio que isso não é exatamente o que você está procurando, mas - por curiosidade e por uma questão de integridade - eu queria ver se eu poderia escrever uma abordagem somente CSS para esse efeito.

Quase ... mas acontece que eu ainda tinha que incluir uma única linha de javascript.

Exemplo de trabalho:

document.querySelector('.box').addEventListener('animationend', (e) => e.target.blur());
.box {
  width: 100px;
  height: 100px;
  border-radius: 5px;
  background-color: #121212;
  cursor: pointer;
}

.box:focus {
 animation: boxAnimation 2s ease;
}

@keyframes boxAnimation {
  100% {transform: translateX(100px);}
}
<div class="box" tabindex="0"></div>

Rounin
fonte
-1

Acredito que seu problema seja exatamente o que .thenvocê está definindo transitioncomo '', quando deveria defini-lo nonecomo no retorno de chamada do timer.

const box = document.querySelector('.box');
box.addEventListener('click', e => {
  if (!box.style.transform) {
    box.style.transform = 'translateX(100px)';
    new Promise(resolve => {
      setTimeout(() => {
        box.style.transition = 'none';
        box.style.transform = '';
        resolve('Transition complete');
      }, 2000)
    }).then(() => {
     box.style.transition = 'none'; // <<----
    })
  }
})
.box {
  width: 100px;
  height: 100px;
  border-radius: 5px;
  background-color: #121212;
  transition: all 2s ease;
}
<div class = "box"></div>

Scott Marcus
fonte
11
Não, o OP está configurando ''para aplicar a regra de classe novamente (que foi anulada pela regra do elemento) seu código simplesmente a define para 'none'duas vezes, o que impede a caixa de fazer a transição de volta, mas não restaura sua transição original (a da classe)
Chris G