Mengapa menyetel properti CSS menggunakan Promise.kemudian tidak benar-benar terjadi di blok itu?


11

Silakan coba dan jalankan cuplikan berikut, lalu klik kotaknya.

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>

Apa yang saya harapkan terjadi:

  • Klik terjadi
  • Box mulai menerjemahkan secara horizontal dengan 100px (tindakan ini memakan waktu dua detik)
  • Pada klik, yang baru Promisejuga dibuat. Di dalam kata Promise, setTimeoutfungsi diatur ke 2 detik
  • Setelah tindakan selesai (dua detik berlalu), setTimeoutjalankan fungsi panggilan baliknya dan atur transitionke none. Setelah melakukan itu, setTimeoutjuga kembali transformke nilai aslinya, sehingga menampilkan kotak untuk muncul di lokasi asli.
  • Kotak muncul di lokasi asli tanpa masalah efek transisi di sini
  • Setelah semua selesai, atur transitionnilai kotak kembali ke nilai aslinya

Namun, seperti yang bisa dilihat, transitionnilainya nampaknya tidak noneketika berjalan. Saya tahu bahwa ada metode lain untuk mencapai hal di atas, misalnya menggunakan keyframe dan transitionend, tetapi mengapa ini terjadi? Saya secara eksplisit mengatur transitionkembali ke nilai aslinya hanya setelah itu setTimeoutselesai callback nya, sehingga menyelesaikan Janji.

EDIT

Sesuai permintaan, berikut adalah gif kode yang menampilkan perilaku bermasalah: Masalah


Di browser apa Anda melihat ini? Di Chrome, saya melihat apa yang dimaksudkan.
Terry

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

Bisakah Anda melampirkan gif ke pertanyaan Anda yang menggambarkan masalah ini? Sejauh yang saya tahu itu juga rendering / berperilaku seperti yang diharapkan di Firefox.
Terry

Ketika Janji terselesaikan, transisi asli dipulihkan, tetapi pada titik ini kotak masih berubah. Karena itu ia bertransisi kembali. Anda harus menunggu setidaknya 1 bingkai lagi sebelum mengatur ulang transisi ke nilai asli: jsfiddle.net/khrismuc/3mjwtack
Chris G

Ketika dijalankan beberapa kali, saya berhasil mereproduksi masalah satu kali. Eksekusi transisi tergantung pada apa yang dilakukan komputer di latar belakang. Menambahkan lebih banyak waktu ke penundaan sebelum menyelesaikan janji mungkin membantu.
Teemu

Jawaban:


4

Putaran gaya kumpulan acara berubah. Jika Anda mengubah gaya elemen pada satu baris, browser tidak segera menampilkan perubahan itu; itu akan menunggu hingga frame animasi berikutnya. Inilah sebabnya, misalnya

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

tidak menghasilkan kelap-kelip; browser hanya peduli dengan nilai gaya yang ditetapkan setelah semua Javascript selesai.

Rendering terjadi setelah semua Javascript selesai, termasuk microtasks . The .thenof a Promise muncul dalam mikrotask (yang akan berjalan secara efektif segera setelah semua Javascript lainnya selesai, tetapi sebelum hal lain - seperti rendering - memiliki peluang untuk dijalankan).

Apa yang Anda lakukan adalah menyetel transitionproperti ke ''dalam mikrotask, sebelum browser mulai merender perubahan yang disebabkan oleh style.transform = ''.

Jika Anda mengatur ulang transisi ke string kosong setelah a requestAnimationFrame(yang akan berjalan tepat sebelum pengecatan berikutnya), dan kemudian setelah setTimeout(yang akan berjalan tepat setelah pengecatan berikutnya), itu akan berfungsi seperti yang diharapkan:

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>


Ini jawaban yang saya cari. Terima kasih. Bisakah Anda, mungkin, memberikan tautan yang menjelaskan mekanisme mikrotask dan pengecatan ulang ini ?
Richard

Ini terlihat seperti ringkasan yang bagus: javascript.info/event-loop
SpecificPerformance

2
Itu tidak sepenuhnya benar, tidak. Perulangan acara tidak mengatakan apa pun tentang perubahan gaya. Sebagian besar browser akan mencoba untuk menunggu bingkai lukisan berikutnya ketika mereka bisa, tetapi itu saja. info lebih lanjut ;-)
Kaiido

3

Anda menghadapi variasi transisi tidak berfungsi jika elemen mulai masalah tersembunyi , tetapi langsung di transitionproperti.

Anda dapat merujuk ke jawaban ini untuk memahami bagaimana CSSOM dan DOM ditautkan untuk proses "redraw".
Pada dasarnya, browser umumnya akan menunggu hingga bingkai lukisan berikutnya untuk menghitung ulang semua posisi kotak baru dan dengan demikian menerapkan aturan CSS ke CSSOM.

Jadi di dalam penangan Janji Anda, ketika Anda mengatur ulang transitionto "", transform: ""masih belum dihitung. Ketika akan dihitung, transitionsudah akan direset ke ""dan CSSOM akan memicu transisi untuk pembaruan transformasi.

Namun, kami dapat memaksa browser untuk memicu "reflow" dan karenanya kami dapat membuatnya menghitung ulang posisi elemen Anda, sebelum kami mengatur ulang transisi ke "".

Yang membuat penggunaan Janji tidak perlu:

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>


Dan untuk penjelasan tentang tugas-mikro, seperti Promise.resolve()atau MutationEvents , atau queueMicrotask(), Anda harus memahami bahwa tugas itu akan dijalankan segera setelah tugas saat ini selesai, langkah ke-7 dari model pemrosesan loop-Peristiwa , sebelum langkah rendering .
Jadi dalam kasus Anda, rasanya seperti dijalankan secara serempak.

Ngomong-ngomong, waspadai tugas-tugas mikro dapat memblokir seperti loop sementara:

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

Ya persis, tetapi menanggapi transitionendacara tersebut akan menghindari keharusan untuk menyulitkan kode waktu habis agar sesuai dengan akhir transisi. transitionToPromise.js akan menjanjikan transisi yang memungkinkan Anda untuk menulis transitionToPromise(box, 'transform', 'translateX(100px)').then(() => /* four lines as per answer above */).
Roamer-1888

Dua keuntungan: (1) durasi transisi kemudian dapat dimodifikasi dalam CSS tanpa perlu memodifikasi javascript; (2) transitionToPromise.js dapat digunakan kembali. BTW, saya mencobanya dan berfungsi dengan baik.
Roamer-1888

@ Roamer-1888 ya Anda bisa, meskipun saya pribadi akan menambahkan cek (evt)=>if(evt.propertyName === "transform"){ ...untuk menghindari positif palsu dan saya tidak suka menjanjikan acara semacam itu, karena Anda tidak pernah tahu apakah itu akan pernah memanas (pikirkan kasus seperti someAncestor.hide() ketika transisi berjalan, Janji Anda tidak akan pernah diluncurkan dan transisi Anda akan dinonaktifkan dinonaktifkan. Jadi itu tergantung pada OP untuk menentukan apa yang terbaik untuk mereka, tetapi secara pribadi dan berdasarkan pengalaman, saya sekarang lebih suka waktu tunggu daripada acara transisi.
Kaiido

1
Pro & kontra, kurasa. Bagaimanapun, dengan atau tanpa promisifikasi, jawaban ini jauh lebih bersih daripada yang melibatkan dua setTimeouts dan requestAnimationFrame.
Roamer-1888

Saya punya satu pertanyaan. Anda mengatakan bahwa requestAnimationFrame()akan dipicu sebelum mengecat ulang browser berikutnya. Anda juga menyebutkan bahwa browser umumnya akan menunggu hingga bingkai lukisan berikutnya untuk menghitung ulang semua posisi kotak baru . Namun, Anda masih perlu secara manual memicu reflow paksa (jawaban Anda di tautan pertama). Saya, karenanya, menarik kesimpulan bahwa bahkan ketika requestAnimationFrame()terjadi sesaat sebelum mengecat ulang, browser masih belum menghitung gaya komputasi terbaru; dengan demikian, kebutuhan untuk secara manual memaksa perhitungan ulang gaya. Benar?
Richard

0

Saya menghargai ini bukan yang Anda cari, tapi - karena penasaran dan demi kelengkapan - saya ingin melihat apakah saya bisa menulis pendekatan khusus CSS untuk efek ini.

Hampir ... tetapi ternyata saya masih harus memasukkan satu baris javascript.

Contoh kerja:

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

Saya percaya masalah Anda hanya bahwa dalam Anda .thenAnda menetapkan transitionuntuk '', ketika Anda harus menetapkan ke noneseperti yang Anda lakukan di callback 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
Tidak, OP sedang mengaturnya ''untuk menerapkan aturan kelas lagi (yang ditolak oleh aturan elemen) kode Anda hanya menetapkannya menjadi 'none'dua kali, yang mencegah kotak dari transisi kembali tetapi tidak mengembalikan transisi aslinya (kelas)
Chris G
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.