Warum erfolgt das Festlegen der CSS-Eigenschaft mithilfe von Promise.then nicht tatsächlich im then-Block?

11

Bitte versuchen Sie, das folgende Snippet auszuführen, und klicken Sie dann auf das Feld.

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>

Was ich erwarte:

  • Klicken passiert
  • Box beginnt mit der horizontalen Übersetzung um 100px (diese Aktion dauert zwei Sekunden).
  • Beim Klicken wird auch eine neue Promiseerstellt. Im Inneren Promiseist eine setTimeoutFunktion auf 2 Sekunden eingestellt
  • Nach Abschluss der Aktion (zwei Sekunden sind verstrichen) wird setTimeoutdie Rückruffunktion ausgeführt und transitionauf "Keine" gesetzt. Danach wird setTimeoutauch transformder ursprüngliche Wert wiederhergestellt, sodass das Feld an der ursprünglichen Position angezeigt wird.
  • Die Box erscheint an der ursprünglichen Stelle ohne Übergangseffekt Problem hier
  • Setzen Sie nach all diesen Vorgängen den transitionWert der Box wieder auf den ursprünglichen Wert zurück

Wie jedoch zu sehen ist, transitionscheint der Wert nonebeim Ausführen nicht zu sein . Ich weiß, dass es andere Methoden gibt, um das oben genannte zu erreichen, z. B. die Verwendung von Keyframe und transitionend, aber warum passiert das? Ich habe das transitionZurück explizit auf seinen ursprünglichen Wert gesetzt, nachdem der setTimeoutRückruf beendet wurde, wodurch das Versprechen aufgelöst wurde.

BEARBEITEN

Auf Anfrage ist hier ein GIF des Codes, der das problematische Verhalten anzeigt: Problem

Richard
quelle
In welchem ​​Browser sehen Sie das? Auf Chrome sehe ich, was es beabsichtigt ist.
Terry
@ Terry Firefox 73.0 (64-Bit) für Windows.
Richard
Können Sie Ihrer Frage ein GIF hinzufügen, das das Problem veranschaulicht? Soweit ich das beurteilen kann, wird es auch in Firefox wie erwartet gerendert / verhalten.
Terry
Wenn das Versprechen aufgelöst wird, wird der ursprüngliche Übergang wiederhergestellt, aber zu diesem Zeitpunkt wird die Box noch transformiert. Daher geht es zurück. Sie müssen mindestens 1 weiteren Frame warten, bevor Sie den Übergang auf den ursprünglichen Wert zurücksetzen
Chris G
Bei mehrmaliger Ausführung konnte ich das Problem einmal reproduzieren. Die Ausführung des Übergangs hängt davon ab, was der Computer im Hintergrund tut. Es könnte hilfreich sein, der Verzögerung etwas mehr Zeit hinzuzufügen, bevor das Versprechen gelöst wird.
Teemu

Antworten:

4

Die Ereignisschleife stapelt Stiländerungen . Wenn Sie den Stil eines Elements in einer Zeile ändern, zeigt der Browser diese Änderung nicht sofort an. Es wird bis zum nächsten Animationsframe warten. Deshalb zum Beispiel

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

führt nicht zu Flackern; Der Browser kümmert sich nur um die Stilwerte, die nach Abschluss von Javascript festgelegt wurden.

Das Rendern erfolgt nach Abschluss von Javascript, einschließlich Mikrotasks . Das .thenVersprechen wird in einer Mikrotask ausgeführt (die effektiv ausgeführt wird, sobald alle anderen Javascript-Vorgänge abgeschlossen sind, aber bevor etwas anderes - wie z. B. das Rendern - ausgeführt werden kann).

Sie setzen die transitionEigenschaft ''in der Mikrotask auf, bevor der Browser mit dem Rendern der durch verursachten Änderungen beginnt style.transform = ''.

Wenn Sie den Übergang zur leeren Zeichenfolge nach a requestAnimationFrame(der kurz vor dem nächsten Repaint ausgeführt wird) und dann nach a setTimeout(der unmittelbar nach dem nächsten Repaint ausgeführt wird) zurücksetzen, funktioniert dies wie erwartet:

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>

Bestimmte Leistung
quelle
Dies ist die Antwort, nach der ich gesucht habe. Vielen Dank. Könnten Sie vielleicht einen Link bereitstellen, der diese Mikrotask- und Repaint- Mechanik beschreibt?
Richard
Dies sieht nach einer guten Zusammenfassung aus: javascript.info/event-loop
CertainPerformance
2
Das stimmt nicht ganz nein. Die Ereignisschleife sagt nichts über Stiländerungen aus. Die meisten Browser werden versuchen, auf den nächsten Malrahmen zu warten, wenn sie können, aber das war es auch schon. mehr Infos ;-)
Kaiido
3

Sie sehen sich einer Variation des Übergangs gegenüber, die nicht funktioniert, wenn das Element ein verstecktes Problem startet , sondern direkt auf der transitionEigenschaft.

Sie können sich auf diese Antwort beziehen, um zu verstehen, wie CSSOM und DOM für den "Neuzeichnungsprozess" verknüpft sind.
Grundsätzlich Browser wird in der Regel bis zur nächsten warten Malerei Frame aller neue Box Positionen neu zu berechnen und damit CSS - Regeln auf die CSSOM anzuwenden.

Wenn Sie in Ihrem Promise-Handler das auf zurücksetzen transition, ""wurde das transform: ""noch nicht berechnet. Wenn es berechnet wird, wurde das transitionbereits zurückgesetzt ""und das CSSOM löst den Übergang für die Transformationsaktualisierung aus.

Wir können den Browser jedoch zwingen, einen "Reflow" auszulösen, und so die Position Ihres Elements neu berechnen, bevor wir den Übergang auf zurücksetzen "".

Was die Verwendung des Versprechens ziemlich unnötig macht:

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>


Und für eine Erklärung zu Mikroaufgaben wie Promise.resolve()oder MutationEvents oder queueMicrotask(), Sie müssen verstehen, dass sie ausgeführt werden, sobald die aktuelle Aufgabe erledigt ist, 7. Schritt des Event-Loop-Verarbeitungsmodells , vor den Rendering-Schritten .
In Ihrem Fall ist es also sehr ähnlich, als würde es synchron ausgeführt.

Übrigens, Vorsicht, Mikro-Tasks können so blockierend sein wie eine while-Schleife:

// this will freeze your page just like a while(1) loop
const makeProm = ()=> Promise.resolve().then( makeProm );
Kaiido
quelle
Ja genau, aber wenn Sie auf das transitionendEreignis reagieren, müssen Sie keine Zeitüberschreitung fest codieren, um das Ende des Übergangs zu erreichen. TransitionToPromise.js verspricht den Übergang, der das Schreiben ermöglicht transitionToPromise(box, 'transform', 'translateX(100px)').then(() => /* four lines as per answer above */).
Roamer-1888,
Zwei Vorteile: (1) Die Dauer des Übergangs kann dann in CSS geändert werden, ohne dass das Javascript geändert werden muss. (2) TransitionToPromise.js ist wiederverwendbar. Übrigens habe ich es versucht und es funktioniert gut.
Roamer-1888,
@ Roamer-1888 Ja, das könnten Sie, obwohl ich persönlich einen Scheck hinzufügen würde (evt)=>if(evt.propertyName === "transform"){ ..., um Fehlalarme zu vermeiden, und ich mag es nicht wirklich, solche Ereignisse zu versprechen, weil Sie nie wissen, ob sie jemals ausgelöst werden (denken Sie an einen Fall wie someAncestor.hide() den Übergang). Ihr Versprechen wird niemals ausgelöst und Ihr Übergang wird deaktiviert bleiben. Es liegt also wirklich an OP, zu bestimmen, was für sie am besten ist, aber persönlich und aus Erfahrung bevorzuge ich jetzt Timeouts gegenüber Übergangsereignissen
Kaiido,
1
Vor- und Nachteile, denke ich. In jedem Fall ist diese Antwort mit oder ohne Versprechen weitaus sauberer als eine mit zwei setTimeouts und einem requestAnimationFrame.
Roamer-1888,
Ich habe jedoch eine Frage. Sie sagten, dass requestAnimationFrame()dies kurz vor dem nächsten Browser-Repaint ausgelöst wird . Sie haben auch erwähnt, dass Browser im Allgemeinen bis zum nächsten Malrahmen warten, um alle neuen Boxpositionen neu zu berechnen . Sie mussten jedoch manuell einen erzwungenen Reflow auslösen (Ihre Antwort auf den ersten Link). Ich komme daher zu dem Schluss, dass requestAnimationFrame()der Browser selbst dann, wenn dies kurz vor dem Neulackieren geschieht, noch nicht den neuesten berechneten Stil berechnet hat. Daher muss eine Neuberechnung der Stile manuell erzwungen werden. Richtig?
Richard
0

Ich weiß, dass dies nicht ganz das ist, wonach Sie suchen, aber aus Neugier und der Vollständigkeit halber wollte ich sehen, ob ich einen Nur-CSS-Ansatz für diesen Effekt schreiben kann.

Fast ... aber es stellte sich heraus, dass ich immer noch eine einzige Zeile Javascript einfügen musste.

Arbeitsbeispiel:

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

Ich glaube , das Problem ist nur , dass in Ihren .thenSie setzen transitionauf '', wenn Sie es seine Einstellung sollten , nonewie Sie im Timer - Rückruf taten.

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
quelle
1
Nein, OP setzt es auf ''die Klassenregel anzuwenden wieder (was durch das Element Regel außer Kraft gesetzt wurde) Code setzt einfach es 'none'zweimal, was die Box verhindert zurück Übergang aber nicht den ursprünglichen Übergang wiederzuherzustellen (die Klasse)
Chris G