¿Por qué no se establece la propiedad CSS usando Promise.then en el bloque de entonces?

11

Intente ejecutar el siguiente fragmento y luego haga clic en el cuadro.

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>

Lo que espero que suceda:

  • Click pasa
  • Box comienza a traducir horizontalmente en 100 px (esta acción lleva dos segundos)
  • Al hacer clic, Promisetambién se crea un nuevo . Dentro de dicho Promise, una setTimeoutfunción se establece en 2 segundos
  • Una vez completada la acción (han transcurrido dos segundos), setTimeoutejecuta su función de devolución de llamada y se establece transitionen ninguno. Después de hacer eso, setTimeouttambién vuelve transforma su valor original, haciendo que el cuadro aparezca en la ubicación original.
  • El cuadro aparece en la ubicación original sin ningún problema de efecto de transición aquí
  • Después de que todos terminen, transitionvuelva a establecer el valor del cuadro en su valor original

Sin embargo, como se puede ver, el transitionvalor no parece ser nonecuando se ejecuta. Sé que hay otros métodos para lograr lo anterior, por ejemplo, utilizando fotogramas clave y transitionend, pero ¿por qué sucede esto? Me puse explícitamente el transitionnuevo a su valor original solamente después de la setTimeouttermina su devolución de llamada, resolviendo así la promesa.

EDITAR

Según la solicitud, aquí hay un gif del código que muestra el comportamiento problemático: Problema

Ricardo
fuente
¿En qué navegador estás viendo esto? En Chrome, veo lo que se pretende.
Terry
@Terry Firefox 73.0 (64 bits) para Windows.
Richard
¿Puedes adjuntar un gif a tu pregunta que ilustre el problema? Por lo que pude ver, también está renderizando / comportándose como se esperaba en Firefox.
Terry
Cuando se resuelve la Promesa, la transición original se restaura, pero en este punto la caja aún se transforma. Por lo tanto, hace la transición de regreso. Debe
Chris G
Cuando lo ejecuté varias veces, logré reproducir el problema una vez. La ejecución de la transición depende de lo que esté haciendo la computadora en segundo plano. Agregar algo más de tiempo al retraso antes de resolver la promesa podría ayudar.
Teemu

Respuestas:

4

El bucle de eventos procesa cambios de estilo. Si cambia el estilo de un elemento en una línea, el navegador no muestra ese cambio de inmediato; esperará hasta el próximo cuadro de animación. Por eso, por ejemplo

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

no produce parpadeo; el navegador solo se preocupa por los valores de estilo establecidos después de que se haya completado todo Javascript.

La representación se produce después de que se haya completado todo Javascript, incluidas las microtask . El .thende una promesa se produce en una microtask (que se ejecutará de manera efectiva tan pronto como todos los demás Javascript hayan terminado, pero antes de que cualquier otra cosa, como el renderizado, haya tenido la oportunidad de ejecutarse).

Lo que está haciendo es establecer la transitionpropiedad ''en microtask, antes de que el navegador haya comenzado a representar el cambio causado por style.transform = ''.

Si restablece la transición a la cadena vacía después de un requestAnimationFrame(que se ejecutará justo antes de la próxima pintura), y luego después de un setTimeout(que se ejecutará justo después de la próxima pintura), funcionará como se esperaba:

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>

Cierto rendimiento
fuente
Esta es la respuesta que estaba buscando. Gracias. ¿Podría, tal vez, proporcionar un enlace que describa estas microtask y la mecánica de repintado ?
Richard
Esto parece un buen resumen: javascript.info/event-loop
CertainPerformance
2
Eso no es exactamente cierto no. El bucle de eventos no dice nada sobre los cambios de estilo. La mayoría de los navegadores intentarán esperar el próximo cuadro de pintura cuando puedan, pero eso es todo. más información ;-)
Kaiido
3

Se enfrenta a una variación de la transición que no funciona si el elemento comienza a ocultarse problema , sino directamente en la transitionpropiedad.

Puede consultar esta respuesta para comprender cómo se vinculan el CSSOM y el DOM para el proceso de "redibujo".
Básicamente, los navegadores generalmente esperan hasta el próximo cuadro de pintura para recalcular todas las nuevas posiciones de la caja y, por lo tanto, para aplicar las reglas CSS al CSSOM.

Así en el controlador Promise, cuando se restablece el transitionque "", el transform: ""todavía no se ha calculado todavía. Cuando se calculará, eltransition ya se habrá restablecido ""y el CSSOM activará la transición para la actualización de la transformación.

Sin embargo, podemos obligar al navegador a activar un "reflujo" y, por lo tanto, podemos hacer que vuelva a calcular la posición de su elemento, antes de restablecer la transición a "".

Lo que hace que el uso de la promesa sea bastante innecesario:

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>


Y para obtener una explicación sobre las micro tareas, como Promise.resolve()o MutationEvents , o bien queueMicrotask(), debe comprender que se ejecutarán tan pronto como se complete la tarea actual, séptimo paso del modelo de procesamiento de bucle de eventos , antes de los pasos de representación .
Entonces, en su caso, es muy parecido a si se ejecutara sincrónicamente.

Por cierto, cuidado con las micro tareas pueden ser tan bloqueadoras como un ciclo while:

// this will freeze your page just like a while(1) loop
const makeProm = ()=> Promise.resolve().then( makeProm );
Kaiido
fuente
Sí exactamente, pero responder al transitionendevento evitaría tener que codificar un tiempo de espera para que coincida con el final de la transición. transitionToPromise.js promete la transición permitiéndole escribir transitionToPromise(box, 'transform', 'translateX(100px)').then(() => /* four lines as per answer above */).
Roamer-1888
Dos ventajas: (1) la duración de la transición se puede modificar en CSS sin necesidad de modificar el javascript; (2) transitionToPromise.js es reutilizable. Por cierto, lo probé y funciona bien.
Roamer-1888
@ Roamer-1888 sí, podría, aunque personalmente agregaría un cheque (evt)=>if(evt.propertyName === "transform"){ ...para evitar falsos positivos y realmente no me gusta prometer tales eventos, porque nunca se sabe si alguna vez se disparará (piense en un caso como someAncestor.hide() cuando se ejecuta la transición, su promesa nunca se activará y su transición se quedará bloqueada, por lo que depende de OP determinar qué es lo mejor para ellos, pero personalmente y por experiencia, ahora prefiero los tiempos de espera que los eventos de transición
Kaiido
1
Pros y contras, supongo. En cualquier caso, con o sin promisificación, esta respuesta es mucho más limpia que una que involucra dos setTimeouts y un requestAnimationFrame.
Roamer-1888
Aunque tengo una pregunta. Dijiste que requestAnimationFrame()se activará justo antes del próximo repintado del navegador. También mencionó que los navegadores generalmente esperan hasta el próximo cuadro de pintura para volver a calcular todas las nuevas posiciones de la caja . Sin embargo, aún necesitaba activar manualmente un reflujo forzado (su respuesta en el primer enlace). Yo, por lo tanto, llego a la conclusión de que incluso cuando requestAnimationFrame()ocurre justo antes de volver a pintar, el navegador aún no ha calculado el nuevo estilo calculado; por lo tanto, la necesidad de forzar manualmente un recálculo de estilos. ¿Correcto?
Richard
0

Aprecio que esto no sea exactamente lo que estás buscando, pero, por curiosidad y en aras de la exhaustividad, quería ver si podía escribir un enfoque solo de CSS para este efecto.

Casi ... pero resulta que todavía tenía que incluir una sola línea de javascript.

Ejemplo de trabajo

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
fuente
-1

Creo que su problema es sólo que en su .thense está ajustando el transitionque '', cuando debería estar fijándose a nonecomo lo hizo en la devolución de llamada del temporizador.

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
fuente
1
No, OP lo está configurando en '' que vuelva a aplicar la regla de la clase (que fue anulada por la regla del elemento), su código simplemente lo establece 'none'dos veces, lo que evita que la caja vuelva a la transición pero no restaura su transición original (la clase)
Chris G