Pourquoi la définition de la propriété CSS à l'aide de Promise.then ne se produit-elle pas réellement au bloc then?

11

Veuillez essayer d'exécuter l'extrait de code suivant, puis cliquez sur la case.

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>

Ce que j'attends:

  • Le clic se produit
  • La boîte commence à traduire horizontalement par 100 pixels (cette action prend deux secondes)
  • Au clic, un nouveau Promiseest également créé. A l'intérieur Promise, une setTimeoutfonction est réglée sur 2 secondes
  • Une fois l'action terminée (deux secondes se sont écoulées), setTimeoutexécute sa fonction de rappel et définit la valeur transitionnone. Après cela, setTimeoutrevient également transformà sa valeur d'origine, rendant ainsi la boîte à apparaître à l'emplacement d'origine.
  • La boîte apparaît à l'emplacement d'origine sans problème d' effet de transition ici
  • Une fois toutes ces opérations terminées, redéfinissez la transitionvaleur de la boîte à sa valeur d'origine

Cependant, comme on peut le voir, la transitionvaleur ne semble pas être nonelors de l'exécution. Je sais qu'il existe d'autres méthodes pour atteindre ce qui précède, par exemple en utilisant des images clés et transitionend, mais pourquoi cela se produit-il? J'ai explicitement mis le transitiondos à sa valeur d'origine seulement après la setTimeoutfin de son rappel, résolvant ainsi la promesse.

ÉDITER

Selon la demande, voici un gif du code affichant le comportement problématique: Problème

Richard
la source
Dans quel navigateur voyez-vous cela? Sur Chrome, je vois à quoi il est destiné.
Terry
@Terry Firefox 73.0 (64 bits) pour Windows.
Richard
Pouvez-vous joindre un gif à votre question qui illustre le problème? Pour autant que je sache, c'est aussi un rendu / un comportement comme prévu sur Firefox.
Terry
Lorsque la promesse se résout, la transition d'origine est restaurée, mais à ce stade, la boîte est toujours transformée. Il revient donc en arrière. Vous devez attendre au moins 1 image supplémentaire avant de réinitialiser la transition à la valeur d'origine: jsfiddle.net/khrismuc/3mjwtack
Chris G
Lorsqu'il a été exécuté plusieurs fois, j'ai réussi à reproduire le problème une fois. L'exécution de la transition dépend de ce que l'ordinateur fait en arrière-plan. Ajouter un peu plus de temps au délai avant de résoudre la promesse pourrait aider.
Teemu

Réponses:

4

La boucle d'événements regroupe les changements de style. Si vous modifiez le style d'un élément sur une ligne, le navigateur n'affiche pas ce changement immédiatement; il attendra la prochaine image d'animation. C’est pourquoi, par exemple

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

n'entraîne pas de scintillement; le navigateur se soucie uniquement des valeurs de style définies une fois que tout Javascript est terminé.

Le rendu se produit une fois que tout Javascript est terminé, y compris les microtâches . La .thenpromesse se produit dans une microtâche (qui s'exécutera effectivement dès que tous les autres Javascript auront terminé, mais avant toute autre chose - comme le rendu - a eu une chance de s'exécuter).

Ce que vous faites, c'est que vous définissez la transitionpropriété ''dans la microtâche, avant que le navigateur ne commence à rendre la modification provoquée par style.transform = ''.

Si vous réinitialisez la transition vers la chaîne vide après a requestAnimationFrame(qui s'exécutera juste avant la prochaine repeinture), puis après a setTimeout(qui s'exécutera juste après la prochaine repeinture), cela fonctionnera comme prévu:

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
la source
C'est la réponse que je cherchais. Merci. Pourriez-vous, peut-être, fournir un lien qui décrit ces microtâches et repeindre la mécanique?
Richard
Cela ressemble à un bon résumé: javascript.info/event-loop
CertainPerformance
2
Ce n'est pas exactement vrai non. La boucle d'événement ne dit rien sur les changements de style. La plupart des navigateurs essaieront d'attendre le prochain cadre de peinture lorsqu'ils le pourront, mais c'est à peu près tout. plus d'infos ;-)
Kaiido
3

Vous êtes confronté à une variation de la transition qui ne fonctionne pas si l'élément commence par un problème caché , mais directement sur la transitionpropriété.

Vous pouvez vous référer à cette réponse pour comprendre comment le CSSOM et le DOM sont liés pour le processus de "redessiner".
Fondamentalement, les navigateurs attendent généralement le prochain cadre de peinture pour recalculer toutes les nouvelles positions de boîte et ainsi appliquer les règles CSS au CSSOM.

Ainsi, dans votre gestionnaire Promise, lorsque vous réinitialisez le transitionà "", le transform: ""n'a toujours pas été calculé. Lorsqu'il sera calculé, le transitionsera déjà réinitialisé ""et le CSSOM déclenchera la transition pour la mise à jour de la transformation.

Cependant, nous pouvons forcer le navigateur à déclencher une "refusion" et ainsi nous pouvons lui faire recalculer la position de votre élément, avant de réinitialiser la transition vers "".

Ce qui rend l'utilisation de la Promesse tout à fait inutile:

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>


Et pour une explication sur les micro-tâches, comme Promise.resolve()ou MutationEvents , ou queueMicrotask(), vous devez comprendre qu'elles seront exécutées dès que la tâche en cours sera terminée, 7ème étape du modèle de traitement de boucle d'événement , avant les étapes de rendu .
Donc, dans votre cas, c'est très similaire à une exécution synchrone.

Soit dit en passant, attention aux micro-tâches peuvent être aussi bloquantes qu'une boucle while:

// this will freeze your page just like a while(1) loop
const makeProm = ()=> Promise.resolve().then( makeProm );
Kaiido
la source
Oui exactement, mais répondre à l' transitionendévénement éviterait d'avoir à coder en dur un délai d'expiration correspondant à la fin de la transition. transitionToPromise.js promettra la transition vous permettant d'écrire transitionToPromise(box, 'transform', 'translateX(100px)').then(() => /* four lines as per answer above */).
Roamer-1888
Deux avantages: (1) la durée de la transition peut alors être modifiée en CSS sans avoir besoin de modifier le javascript; (2) transitionToPromise.js est réutilisable. BTW, je l'ai essayé et ça marche bien.
Roamer-1888
@ Roamer-1888 oui, vous pourriez le faire, même si j'ajouterais personnellement une vérification (evt)=>if(evt.propertyName === "transform"){ ...pour éviter les faux positifs et je n'aime pas vraiment promettre de tels événements, car vous ne savez jamais si cela se déclenchera jamais (pensez à un cas comme someAncestor.hide() lorsque la transition s'exécute, votre promesse ne se déclenchera jamais et votre transition restera bloquée. C'est donc à OP de déterminer ce qui est le mieux pour eux, mais personnellement et par expérience, je préfère maintenant les délais d'attente que les événements de transition.
Kaiido
1
Pour et contre, je suppose. Dans tous les cas, avec ou sans promisification, cette réponse est bien plus nette que celle impliquant deux setTimeouts et un requestAnimationFrame.
Roamer-1888
J'ai quand même une question. Vous avez dit que requestAnimationFrame()cela se déclencherait juste avant la prochaine repeinture du navigateur. Vous avez également mentionné que les navigateurs attendront généralement le prochain cadre de peinture pour recalculer toutes les nouvelles positions de boîte . Pourtant, vous aviez encore besoin de déclencher manuellement une refusion forcée (votre réponse sur le premier lien). Par conséquent, je tire la conclusion que même lorsque requestAnimationFrame()cela se produit juste avant de repeindre, le navigateur n'a toujours pas calculé le nouveau style calculé; ainsi, la nécessité de forcer manuellement un recalcul des styles. Correct?
Richard
0

J'apprécie que ce n'est pas tout à fait ce que vous recherchez, mais - par curiosité et par souci d'exhaustivité - je voulais voir si je pouvais écrire une approche CSS uniquement à cet effet.

Presque ... mais il s'avère que je devais encore inclure une seule ligne de javascript.

Exemple de travail:

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
la source
-1

Je crois que votre problème est simplement que .thenvous définissez le transitionsur '', alors que vous devriez le régler nonecomme vous l'avez fait dans le rappel de la minuterie.

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
la source
1
Non, OP le définit ''pour appliquer à nouveau la règle de classe (qui a été annulée par la règle d'élément), votre code la définit simplement 'none'deux fois, ce qui empêche la boîte de revenir en arrière mais ne restaure pas sa transition d'origine (celle de la classe)
Chris G