Dlaczego ustawianie właściwości CSS przy użyciu Promise.the tak naprawdę nie dzieje się w tym bloku?

11

Spróbuj uruchomić następujący fragment kodu, a następnie kliknij pole.

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>

Czego się spodziewam:

  • Kliknięcie się dzieje
  • Box zaczyna tłumaczyć w poziomie o 100 pikseli (ta czynność zajmuje dwie sekundy)
  • Po kliknięciu Promisetworzony jest również nowy . Wewnątrz powiedział Promise, setTimeoutfunkcja jest ustawiona na 2 sekundy
  • Po zakończeniu akcji (po upływie dwóch sekund) setTimeouturuchamia funkcję zwrotną i ustawia ją transitionna none. Po wykonaniu tej czynności setTimeoutrównież przywraca się transformdo pierwotnej wartości, co powoduje, że pole pojawi się w oryginalnej lokalizacji.
  • Pole pojawi się w oryginalnej lokalizacji bez problemu z efektem przejścia
  • Po zakończeniu wszystkich tych czynności ustaw transitionwartość pola z powrotem na pierwotną

Jednak, jak można zauważyć, transitionwydaje się , że wartość nie jest noneuruchomiona. Wiem, że istnieją inne metody osiągnięcia powyższego, np. Użycie klatki kluczowej i transitionend, ale dlaczego tak się dzieje? I jawnie ustawić transitionz powrotem do jego pierwotnej wartości tylko posetTimeout zakończy zwrotnego, a więc rozwiązania obietnicy.

EDYTOWAĆ

Jak na żądanie, oto gif kodu wyświetlającego problematyczne zachowanie: Problem

Richard
źródło
W jakiej przeglądarce to widzisz? W Chrome widzę, co jest przeznaczone.
Terry
@Terry Firefox 73.0 (64-bit) dla systemu Windows.
Richard
Czy możesz dołączyć gif do swojego pytania, które ilustruje problem? O ile mogłem powiedzieć, renderuje / zachowuje się zgodnie z oczekiwaniami w przeglądarce Firefox.
Terry
Kiedy obietnica zostanie rozwiązana, oryginalne przejście zostanie przywrócone, ale w tym momencie pudełko jest nadal przekształcane. Dlatego przechodzi z powrotem. Przed zresetowaniem przejścia do pierwotnej wartości musisz poczekać co najmniej 1 ramkę: jsfiddle.net/khrismuc/3mjwtack
Chris G
Po wielokrotnym uruchomieniu udało mi się raz odtworzyć problem. Wykonanie przejścia zależy od tego, co robi komputer w tle. Pomocne może być wydłużenie czasu oczekiwania na rozwiązanie obietnicy.
Teemu

Odpowiedzi:

4

Impreza pętla partie styl zmienia. Jeśli zmienisz styl elementu w jednym wierszu, przeglądarka nie pokaże tej zmiany natychmiast; będzie czekać do następnej klatki animacji. Dlatego na przykład

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

nie powoduje migotania; przeglądarka dba tylko o wartości stylu ustawione po zakończeniu wszystkich skryptów Javascript.

Renderowanie następuje po zakończeniu wszystkich skryptów Javascript, w tym mikroprocesorów . .thenPromesy występuje w microtask (która będzie skutecznie prowadzony tak szybko, jak wszystkie inne Javascript zakończył, ale zanim cokolwiek innego - takie jak renderowania - miał okazję do uruchomienia).

To, co robisz, to ustawianie transitionwłaściwości ''w mikroprocesorze, zanim przeglądarka zacznie renderować zmianę spowodowaną przez style.transform = ''.

Jeśli zresetujesz przejście do pustego ciągu po znaku requestAnimationFrame(który uruchomi się tuż przed następnym przemalowaniem), a następnie po znaku setTimeout(który uruchomi się tuż po następnym odmalowaniu), będzie działał zgodnie z oczekiwaniami:

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
źródło
Oto odpowiedź, której szukałem. Dzięki. Mógłbyś być może podać link, który opisuje te microtask i przemalować mechaniki?
Richard
To wygląda na dobre podsumowanie: javascript.info/event-loop
CertainPerformance
2
To nie do końca prawda. Pętla zdarzeń nie mówi nic o zmianach stylu. Większość przeglądarek spróbuje poczekać na kolejną ramkę malowania, kiedy będzie to możliwe, ale to wszystko. więcej informacji ;-)
Kaiido
3

Masz do czynienia z odmianą przejścia nie działa, jeśli element zaczyna ukryty problem, ale bezpośrednio na transitionwłaściwości.

Możesz odnieść się do tej odpowiedzi, aby zrozumieć, w jaki sposób CSSOM i DOM są połączone w procesie „przerysowywania”.
Zasadniczo przeglądarki zazwyczaj czekają do następnej ramki malowania, aby ponownie obliczyć wszystkie nowe pozycje ramek, a tym samym zastosować reguły CSS do CSSOM.

Tak więc w twoim programie obsługi obietnic, po zresetowaniu transitiondo "", transform: ""nadal nie zostało obliczone. Kiedy zostanie obliczony, transitionzostaną już zresetowane, ""a CSSOM uruchomi przejście do aktualizacji transformacji.

Możemy jednak zmusić przeglądarkę do uruchomienia „ ponownego wlania”, a zatem możemy ponownie obliczyć pozycję twojego elementu, zanim zresetujemy przejście do "".

Co sprawia, że ​​korzystanie z Obietnicy jest zupełnie niepotrzebne:

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>


Oraz o wyjaśnienie mikro zadań, jak Promise.resolve()i MutationEvents czy queueMicrotask()trzeba zrozumieć dostaną Ran jak tylko prąd zadanie jest wykonane, 7. etap modelu przetwarzania zdarzeń pętli przed schodach renderingu .
W twoim przypadku wygląda to tak, jakby było uruchamiane synchronicznie.

Nawiasem mówiąc, uważajcie mikroprocesory mogą być tak samo blokujące jak pętla while:

// this will freeze your page just like a while(1) loop
const makeProm = ()=> Promise.resolve().then( makeProm );
Kaiido
źródło
Tak, dokładnie, ale reakcja na transitionendzdarzenie pozwoliłaby uniknąć zakodowania limitu czasu w celu dopasowania do końca przejścia. przejścieToPromise.js obiecuje przejście, umożliwiając pisanie transitionToPromise(box, 'transform', 'translateX(100px)').then(() => /* four lines as per answer above */).
Roamer-1888
Dwie zalety: (1) czas przejścia można następnie zmodyfikować w CSS bez potrzeby modyfikacji javascript; (2) przejścieToPromise.js jest wielokrotnego użytku. BTW, próbowałem i działa dobrze.
Roamer-1888
@ Roamer-1888 tak, mógłbyś, chociaż osobiście dodam czek, (evt)=>if(evt.propertyName === "transform"){ ...aby uniknąć fałszywych alarmów, i tak naprawdę nie lubię obiecywać takich wydarzeń, ponieważ nigdy nie wiesz, czy to się kiedykolwiek uruchomi (pomyśl o przypadku, someAncestor.hide() gdy nastąpi przejście, Twoja obietnica nigdy nie zadziała, a przejście zostanie zablokowane. Więc to naprawdę zależy od OP, aby ustalić, co jest dla nich najlepsze, ale osobiście i z doświadczenia wolę teraz przerwy niż wydarzenia przejściowe.
Kaiido
1
Chyba za i przeciw. W każdym razie, z obietnicą lub bez, ta odpowiedź jest znacznie bardziej przejrzysta niż ta obejmująca dwa setTimeouts i requestAnimationFrame.
Roamer-1888
Mam jednak jedno pytanie. Powiedziałeś, że requestAnimationFrame()zostanie on uruchomiony tuż przed odświeżeniem następnej przeglądarki. Wspomniałeś również, że przeglądarki zazwyczaj będą czekać do następnej ramki malowania, aby ponownie obliczyć wszystkie nowe pozycje ramek . Nadal jednak musisz ręcznie uruchomić wymuszone ponowne zalanie (odpowiedź na pierwszy link). Dlatego wyciągam wniosek, że nawet jeśli requestAnimationFrame()dzieje się to tuż przed odmalowaniem, przeglądarka nadal nie obliczyła najnowszego obliczonego stylu; stąd potrzeba ręcznego wymuszenia przeliczenia stylów. Poprawny?
Richard
0

Rozumiem, że nie jest to dokładnie to, czego szukasz, ale - z ciekawości i ze względu na kompletność - chciałem sprawdzić, czy mógłbym napisać podejście oparte wyłącznie na CSS.

Prawie ... ale okazuje się, że wciąż musiałem dołączyć jedną linię javascript.

Przykład roboczy:

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
źródło
-1

Uważam, że twój problem polega na tym, że w twoim .thenustawiasz transitionto '', kiedy powinieneś ustawić to nonetak, jak w oddzwanianiu z timerem.

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
źródło
1
Nie, OP ustawia, aby ''ponownie zastosować regułę klasy (która została unieważniona przez regułę elementu), twój kod po prostu ustawia ją na'none' dwa razy, co zapobiega cofnięciu się pudełka, ale nie przywraca pierwotnego przejścia (klasy)
Chris G