Perché l'impostazione della proprietà CSS tramite Promise.then non si verifica effettivamente al blocco allora?


11

Prova a eseguire il seguente frammento, quindi fai clic sulla casella.

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>

Cosa mi aspetto che accada:

  • Il clic accade
  • La casella inizia a tradurre in orizzontale di 100 px (questa azione richiede due secondi)
  • Al clic, Promiseviene anche creato un nuovo . Al suo interno Promise, una setTimeoutfunzione è impostata su 2 secondi
  • Dopo che l'azione è stata completata (sono trascorsi due secondi), setTimeoutesegue la sua funzione di richiamata e imposta transitionsu nessuna. Dopo averlo fatto, setTimeoutripristina anche il transformsuo valore originale, rendendo così la casella per apparire nella posizione originale.
  • La casella viene visualizzata nella posizione originale senza problemi di effetto di transizione qui
  • Al termine di tutte queste operazioni, reimpostare il transitionvalore della casella sul valore originale

Tuttavia, come si può vedere, il transitionvalore non sembra essere in noneesecuzione. So che esistono altri metodi per ottenere quanto sopra, ad es. L'utilizzo di fotogrammi chiave e transitionend, ma perché succede? Ho impostato in modo esplicito transitionl'indietro al suo valore originale solo dopo la setTimeoutcompletato le sue callback, risolvendo così la promessa.

MODIFICARE

Come da richiesta, ecco una gif del codice che mostra il comportamento problematico: Problema


In quale browser stai vedendo questo? Su Chrome, vedo cosa è destinato.
Terry,

@Terry Firefox 73.0 (64-bit) per Windows.
Richard,

Puoi allegare una gif alla tua domanda che illustra il problema? Per quanto ne so è anche il rendering / comportamento come previsto su Firefox.
Terry,

Quando Promise si risolve, la transizione originale viene ripristinata, ma a questo punto la casella viene ancora trasformata. Pertanto torna indietro. Devi attendere almeno un altro fotogramma prima di ripristinare la transizione al valore originale: jsfiddle.net/khrismuc/3mjwtack
Chris G

Quando eseguito più volte, sono riuscito a riprodurre il problema una volta. L'esecuzione della transizione dipende da ciò che il computer sta facendo in background. Aggiungere un po 'più di tempo al ritardo prima di risolvere la promessa potrebbe aiutare.
Teemu,

Risposte:


4

Lo stile batch del ciclo di eventi cambia. Se si modifica lo stile di un elemento su una riga, il browser non mostra immediatamente tale modifica; attenderà fino al prossimo fotogramma di animazione. Questo è il motivo, ad esempio

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

non provoca sfarfallio; il browser si preoccupa solo dei valori di stile impostati dopo che tutto Javascript è stato completato.

Il rendering avviene dopo che JavaScript è stato completato, inclusi i microtask . The .thenof a Promise si presenta in un microtask (che verrà effettivamente eseguito non appena tutti gli altri Javascript sono terminati, ma prima che qualsiasi altra cosa, come il rendering, abbia avuto la possibilità di essere eseguita).

Quello che stai facendo è impostare la transitionproprietà su ''nel microtask, prima che il browser abbia iniziato a rendere la modifica causata da style.transform = ''.

Se reimposti la transizione sulla stringa vuota dopo un requestAnimationFrame(che verrà eseguito subito prima del prossimo ridipinto), e quindi dopo un setTimeout(che verrà eseguito subito dopo il successivo ridipinto), funzionerà come previsto:

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>


Questa è la risposta che stavo cercando. Grazie. Potresti forse fornire un link che descriva questi microtask e ridipingere la meccanica?
Richard,

Sembra un buon riassunto: javascript.info/event-loop
CertainPerformance

2
Non è esattamente vero no. Il ciclo degli eventi non dice nulla sui cambiamenti di stile. La maggior parte dei browser proverà ad attendere il prossimo riquadro di pittura quando può, ma questo è tutto. maggiori informazioni ;-)
Kaiido

3

Stai affrontando una variazione della transizione non funziona se l'elemento avvia un problema nascosto , ma direttamente sulla transitionproprietà.

Puoi fare riferimento a questa risposta per capire come CSSOM e DOM sono collegati per il processo di "ridisegno".
Fondamentalmente, i browser generalmente aspetteranno fino al prossimo riquadro di disegno per ricalcolare tutte le nuove posizioni del riquadro e quindi applicare le regole CSS al CSSOM.

Pertanto, nel gestore Promise, quando si reimposta il transitionto "", transform: ""non è stato ancora calcolato. Quando verrà calcolato, transitionsarà già stato reimpostato su ""e CSSOM attiverà la transizione per l'aggiornamento della trasformazione.

Tuttavia, possiamo forzare il browser a innescare un "reflow" e quindi possiamo farlo ricalcolare la posizione del tuo elemento, prima di reimpostare la transizione su "".

Il che rende l'uso della Promessa abbastanza 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>


E per una spiegazione su micro-task, come Promise.resolve()o MutationEvents , oppure queueMicrotask(), devi capire che verranno eseguiti non appena l'attività corrente viene eseguita, 7 ° passaggio del modello di elaborazione del ciclo di eventi , prima dei passaggi di rendering .
Quindi, nel tuo caso, è molto simile a se fosse eseguito in modo sincrono.

A proposito, attenzione ai micro-task possono essere bloccanti come un ciclo while:

// this will freeze your page just like a while(1) loop
const makeProm = ()=> Promise.resolve().then( makeProm );

Sì, esattamente, ma rispondere transitionendall'evento eviterebbe di dover programmare un timeout per abbinare la fine della transizione. transizioneToPromise.js promuoverà la transizione consentendoti di scrivere transitionToPromise(box, 'transform', 'translateX(100px)').then(() => /* four lines as per answer above */).
Roamer-1888,

Due vantaggi: (1) la durata della transizione può quindi essere modificata in CSS senza dover modificare il javascript; (2) transizioneToPromise.js è riutilizzabile. A proposito, l'ho provato e funziona bene.
Roamer-1888,

@ Roamer-1888 sì, potresti, anche se aggiungerei personalmente un assegno (evt)=>if(evt.propertyName === "transform"){ ...per evitare falsi positivi e non mi piace molto promuovere eventi del genere, perché non sai mai se sparerà mai (pensa a un caso come someAncestor.hide() quando si verifica la transizione, la promessa non sarà mai il fuoco e la transizione sarà rimanere bloccati disabilitato ecco, questo è davvero a OP per determinare ciò che è meglio per loro, ma personalmente e per esperienza, io ora preferisco timeout di eventi transitionend..
Kaiido

1
Pro e contro, immagino. In ogni caso, con o senza promisificazione, questa risposta è molto più chiara di una che coinvolge due setTimeouts e una requestAnimationFrame.
Roamer-1888,

Ho una domanda però. Hai detto che requestAnimationFrame()verrà attivato appena prima del prossimo ridisegno del browser. Hai anche detto che i browser generalmente aspetteranno fino al prossimo riquadro di pittura per ricalcolare tutte le nuove posizioni del riquadro . Tuttavia, era ancora necessario attivare manualmente un riflusso forzato (la risposta sul primo collegamento). Ho, quindi, trarre la conclusione che, anche quando requestAnimationFrame()accade poco prima riverniciare, il browser ha ancora non ha calcolato il nuovo stile calcolata; quindi, la necessità di forzare manualmente un ricalcolo degli stili. Corretta?
Richard,

0

Apprezzo che questo non sia proprio quello che stai cercando, ma - per curiosità e per completezza - volevo vedere se potevo scrivere un approccio solo CSS a questo effetto.

Quasi ... ma si scopre che dovevo ancora includere una sola riga di javascript.

Esempio di lavoro:

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>


-1

Credo che il tuo problema sia proprio quello .thenche stai impostando transitionsu '', quando dovresti impostarlo nonecome hai fatto nel callback del 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>


1
No, OP lo sta impostando ''per applicare nuovamente la regola di classe (che è stata annullata dalla regola dell'elemento) il tuo codice semplicemente lo imposta su 'none'due volte, il che impedisce al box di tornare indietro ma non ripristina la sua transizione originale (quella della classe)
Chris G,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.