Mengapa setTimeout (fn, 0) terkadang berguna?

872

Saya baru-baru ini mengalami bug yang agak jahat, di mana kode itu memuat secara <select>dinamis melalui JavaScript. Ini dimuat secara dinamis <select>memiliki nilai yang dipilih sebelumnya. Di IE6, kami sudah memiliki kode untuk memperbaiki yang dipilih <option>, karena kadang-kadang <select>'s selectedIndexnilai akan keluar dari sinkron dengan yang dipilih <option>' s indexatribut, seperti di bawah ini:

field.selectedIndex = element.index;

Namun, kode ini tidak berfungsi. Meskipun bidang selectedIndexitu disetel dengan benar, indeks yang salah akan dipilih. Namun, jika saya memasukkan alert()pernyataan pada saat yang tepat, opsi yang benar akan dipilih. Berpikir ini mungkin semacam masalah waktu, saya mencoba sesuatu yang acak yang saya lihat dalam kode sebelumnya:

var wrapFn = (function() {
    var myField = field;
    var myElement = element;

    return function() {
        myField.selectedIndex = myElement.index;
    }
})();
setTimeout(wrapFn, 0);

Dan ini berhasil!

Saya punya solusi untuk masalah saya, tetapi saya tidak nyaman karena saya tidak tahu persis mengapa ini memperbaiki masalah saya. Adakah yang punya penjelasan resmi? Masalah browser apa yang saya hindari dengan memanggil fungsi saya "nanti" menggunakan setTimeout()?

Dan Lew
sumber
2
Sebagian besar pertanyaan menjelaskan mengapa ini berguna. Jika Anda perlu tahu mengapa ini terjadi - baca jawaban saya: stackoverflow.com/a/23747597/1090562
Salvador Dali
18
Philip Roberts menjelaskan ini dengan cara yang terbaik di sini dalam ceramahnya, "Apa-apaan acara itu?" youtube.com/watch?v=8aGhZQkoFbQ
vasa
Jika Anda tergesa-gesa, ini adalah bagian dari video yang mulai ia jawab dengan tepat: youtu.be/8aGhZQkoFbQ?t=14m54s . Regarldless, seluruh video pasti layak ditonton.
William Hampshire
4
setTimeout(fn)sama dengan setTimeout(fn, 0), btw.
vsync

Jawaban:

827

Dalam pertanyaan tersebut, ada kondisi ras antara:

  1. Upaya browser untuk menginisialisasi daftar drop-down, siap untuk memperbarui indeks yang dipilih, dan
  2. Kode Anda untuk mengatur indeks yang dipilih

Kode Anda secara konsisten memenangkan perlombaan ini dan mencoba untuk mengatur pilihan drop-down sebelum browser siap, yang berarti bahwa bug akan muncul.

Ras ini ada karena JavaScript memiliki utas eksekusi tunggal yang dibagikan dengan rendering halaman. Akibatnya, menjalankan JavaScript memblokir pembaruan DOM.

Solusi Anda adalah:

setTimeout(callback, 0)

Memanggil setTimeoutdengan callback, dan nol sebagai argumen kedua akan menjadwalkan panggilan balik untuk dijalankan secara tidak sinkron , setelah penundaan sesingkat mungkin - yang akan menjadi sekitar 10 ms ketika tab memiliki fokus dan utas eksekusi JavaScript tidak sibuk.

Solusi OP, oleh karena itu untuk menunda sekitar 10 ms, pengaturan indeks yang dipilih. Ini memberi browser kesempatan untuk menginisialisasi DOM, memperbaiki bug.

Setiap versi Internet Explorer menunjukkan perilaku yang aneh dan penyelesaian semacam ini kadang-kadang diperlukan. Atau mungkin itu adalah bug asli dalam basis kode OP.


Lihat Philip Roberts bicara "Apa-apaan acara itu?" untuk penjelasan yang lebih menyeluruh.

ahli statika
sumber
276
'Solusinya adalah "menjeda" eksekusi JavaScript untuk membiarkan utas penguraian mengejar.' Tidak sepenuhnya benar, yang dilakukan setTimeout adalah menambahkan acara baru ke antrean acara browser dan mesin rendering sudah ada dalam antrian itu (tidak sepenuhnya benar, tapi cukup dekat) sehingga dieksekusi sebelum acara setTimeout.
David Mulder
48
Ya, itu adalah jawaban yang jauh lebih terperinci dan jauh lebih benar. Tapi milikku "cukup benar" bagi orang untuk memahami mengapa trik ini berhasil.
Statika
2
@ Davidvidul, apakah ini berarti browser mengurai css dan merender di thread yang berbeda dari thread eksekusi javascript?
jason
8
Tidak, mereka diuraikan secara prinsip di utas yang sama jika tidak, beberapa baris manipulasi DOM akan memicu reflows setiap saat yang akan memiliki pengaruh yang sangat buruk pada kecepatan eksekusi.
David Mulder
30
Video ini adalah penjelasan terbaik mengapa kami setTimeout 0 2014.jsconf.eu/speakers/…
davibq
665

Kata pengantar:

Beberapa jawaban lain benar tetapi tidak benar-benar menggambarkan apa masalah yang sedang dipecahkan, jadi saya membuat jawaban ini untuk menyajikan ilustrasi terperinci itu.

Karena itu, saya memposting perincian terperinci tentang apa yang dilakukan browser dan bagaimana menggunakan setTimeout()bantuan . Itu terlihat gondrong tetapi sebenarnya sangat sederhana dan mudah - saya hanya membuatnya sangat rinci.

UPDATE: Saya telah membuat JSFiddle untuk live-mendemonstrasikan penjelasan di bawah ini: http://jsfiddle.net/C2YBE/31/ . Banyak terima kasih kepada @ThangChung untuk membantu memulainya.

UPDATE2: Kalau-kalau situs web JSFiddle mati, atau menghapus kode, saya menambahkan kode ke jawaban ini di akhir.


RINCIAN :

Bayangkan aplikasi web dengan tombol "lakukan sesuatu" dan hasil div.

The onClickhandler untuk tombol "melakukan sesuatu" panggilan fungsi "LongCalc ()", yang melakukan 2 hal:

  1. Menghitung sangat lama

  2. Mencetak hasil perhitungan ke dalam div hasil.

Sekarang, pengguna Anda mulai menguji ini, klik tombol "lakukan sesuatu", dan halaman yang tampaknya tidak melakukan apa-apa selama 3 menit, mereka gelisah, klik lagi tombol, tunggu 1 menit, tidak ada yang terjadi, klik tombol lagi ...

Masalahnya jelas - Anda menginginkan DIV "Status", yang menunjukkan apa yang terjadi. Mari kita lihat cara kerjanya.


Jadi Anda menambahkan "Status" DIV (awalnya kosong), dan memodifikasi onclickhandler (fungsi LongCalc()) untuk melakukan 4 hal:

  1. Isi status "Menghitung ... mungkin memerlukan waktu ~ 3 menit" ke status DIV

  2. Menghitung sangat lama

  3. Mencetak hasil perhitungan ke dalam div hasil.

  4. Mengisi status "Perhitungan selesai" ke dalam status DIV

Dan, Anda dengan senang hati memberikan aplikasi kepada pengguna untuk diuji ulang.

Mereka kembali padamu tampak sangat marah. Dan jelaskan bahwa ketika mereka mengklik tombol, Status DIV tidak pernah diperbarui dengan status "Menghitung ..." !!!


Anda menggaruk-garuk kepala, bertanya-tanya di StackOverflow (atau membaca dokumen atau google), dan menyadari masalahnya:

Browser menempatkan semua tugas "TODO" (baik tugas UI dan perintah JavaScript) yang dihasilkan dari peristiwa ke dalam satu antrian . Dan sayangnya, menggambar ulang DIV "Status" dengan nilai "Menghitung ..." yang baru adalah TODO terpisah yang menuju akhir antrian!

Berikut ini rincian peristiwa selama pengujian pengguna Anda, konten antrian setelah setiap peristiwa:

  • Antre: [Empty]
  • Acara: Klik tombol. Antrian setelah acara:[Execute OnClick handler(lines 1-4)]
  • Peristiwa: Jalankan baris pertama di penangan OnClick (mis. Ubah nilai DIV Status). Antrian setelah acara: [Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value]. Harap perhatikan bahwa meskipun perubahan DOM terjadi secara instan, untuk menggambar ulang elemen DOM yang sesuai, Anda memerlukan acara baru, yang dipicu oleh perubahan DOM, yang berlangsung di akhir antrian .
  • MASALAH!!! MASALAH!!! Detail dijelaskan di bawah ini.
  • Peristiwa: Jalankan baris kedua di handler (perhitungan). Antrian setelah: [Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value].
  • Acara: Jalankan baris ke-3 di handler (populasikan hasil DIV). Antrian setelah: [Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result].
  • Acara: Jalankan baris ke-4 di handler (mengisi status DIV dengan "DONE"). Antrian: [Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value].
  • Peristiwa: jalankan tersirat returndari onclicksub handler. Kami mengambil "Execute OnClick handler" dari antrian dan mulai menjalankan item berikutnya pada antrian.
  • CATATAN: Karena kami sudah menyelesaikan perhitungan, 3 menit telah berlalu untuk pengguna. Acara undian ulang belum terjadi !!!
  • Acara: menggambar ulang Status DIV dengan nilai "Menghitung". Kami melakukan undian ulang dan melepasnya dari antrian.
  • Peristiwa: menggambar ulang DIV Hasil dengan nilai hasil. Kami melakukan undian ulang dan melepasnya dari antrian.
  • Acara: menggambar ulang Status DIV dengan nilai "Selesai". Kami melakukan undian ulang dan melepasnya dari antrian. Pemirsa yang bermata tajam bahkan mungkin memperhatikan "Status DIV dengan" Menghitung "nilai berkedip untuk sebagian kecil mikrodetik - SETELAH PERHITUNGAN SELESAI

Jadi, masalah yang mendasarinya adalah bahwa acara undian ulang untuk "Status" DIV ditempatkan pada antrian di akhir, SETELAH acara "eksekusi garis 2" yang membutuhkan waktu 3 menit, sehingga pengundian ulang yang sebenarnya tidak terjadi sampai SETELAH perhitungan dilakukan.


Untuk penyelamatan datang setTimeout(). Bagaimana ini membantu? Karena dengan memanggil kode lama-eksekusi via setTimeout, Anda benar-benar membuat 2 peristiwa: setTimeouteksekusi itu sendiri, dan (karena 0 batas waktu), entri entri antrian terpisah untuk kode yang dieksekusi.

Jadi, untuk memperbaiki masalah Anda, Anda memodifikasi onClickhandler Anda menjadi DUA pernyataan (dalam fungsi baru atau hanya satu blok di dalamnya onClick):

  1. Isi status "Menghitung ... mungkin memerlukan waktu ~ 3 menit" ke status DIV

  2. Jalankan setTimeout()dengan 0 batas waktu dan panggilan untuk LongCalc()berfungsi .

    LongCalc()fungsi hampir sama seperti terakhir kali tetapi jelas tidak memiliki status "Menghitung ..." pembaruan DIV sebagai langkah pertama; dan bukannya memulai perhitungan segera.

Jadi, seperti apa urutan kejadian dan antriannya sekarang?

  • Antre: [Empty]
  • Acara: Klik tombol. Antrian setelah acara:[Execute OnClick handler(status update, setTimeout() call)]
  • Peristiwa: Jalankan baris pertama di penangan OnClick (mis. Ubah nilai DIV Status). Antrian setelah acara: [Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value].
  • Acara: Jalankan handler baris kedua (panggilan setTimeout). Antrian setelah: [re-draw Status DIV with "Calculating" value]. Antrian tidak memiliki yang baru di dalamnya selama 0 detik lagi.
  • Acara: Alarm dari batas waktu mati, 0 detik kemudian. Antrian setelah: [re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)].
  • Acara: menggambar ulang Status DIV dengan nilai "Menghitung" . Antrian setelah: [execute LongCalc (lines 1-3)]. Harap dicatat bahwa acara menggambar ulang ini mungkin benar-benar terjadi SEBELUM alarm berbunyi, yang juga berfungsi.
  • ...

Hore! Status DIV baru saja diperbarui ke "Menghitung ..." sebelum perhitungan dimulai !!!



Di bawah ini adalah contoh kode dari JSFiddle yang menggambarkan contoh-contoh ini: http://jsfiddle.net/C2YBE/31/ :

Kode HTML:

<table border=1>
    <tr><td><button id='do'>Do long calc - bad status!</button></td>
        <td><div id='status'>Not Calculating yet.</div></td>
    </tr>
    <tr><td><button id='do_ok'>Do long calc - good status!</button></td>
        <td><div id='status_ok'>Not Calculating yet.</div></td>
    </tr>
</table>

Kode JavaScript: (Dieksekusi pada onDomReadydan mungkin memerlukan jQuery 1.9)

function long_running(status_div) {

    var result = 0;
    // Use 1000/700/300 limits in Chrome, 
    //    300/100/100 in IE8, 
    //    1000/500/200 in FireFox
    // I have no idea why identical runtimes fail on diff browsers.
    for (var i = 0; i < 1000; i++) {
        for (var j = 0; j < 700; j++) {
            for (var k = 0; k < 300; k++) {
                result = result + i + j + k;
            }
        }
    }
    $(status_div).text('calculation done');
}

// Assign events to buttons
$('#do').on('click', function () {
    $('#status').text('calculating....');
    long_running('#status');
});

$('#do_ok').on('click', function () {
    $('#status_ok').text('calculating....');
    // This works on IE8. Works in Chrome
    // Does NOT work in FireFox 25 with timeout =0 or =1
    // DOES work in FF if you change timeout from 0 to 500
    window.setTimeout(function (){ long_running('#status_ok') }, 0);
});
DVK
sumber
12
jawaban bagus DVK! Berikut adalah intisari yang menggambarkan contoh Anda gist.github.com/kumikoda/5552511#file-timeout-html
kumikoda
4
Jawaban yang sangat keren, DVK. Agar mudah dibayangkan, saya telah memasukkan kode itu ke jsfiddle jsfiddle.net/thangchung/LVAaV
thangchung
4
@ThangChung - Saya mencoba membuat versi yang lebih baik (2 tombol, satu untuk setiap case) di JSFiddle. Ini berfungsi sebagai demo di Chrome dan IE tetapi tidak pada FF untuk beberapa alasan - lihat jsfiddle.net/C2YBE/31 . Saya bertanya mengapa FF tidak bekerja di sini: stackoverflow.com/questions/20747591/…
DVK
1
@DVK "Peramban menempatkan semua tugas" TODO "(tugas UI dan perintah JavaScript) yang dihasilkan dari peristiwa ke dalam satu antrian". Pak, bisakah Anda berikan sumbernya untuk ini? Peramban imho seharusnya memiliki UI yang berbeda (rendering engine) dan JS threads .... jangan tersinggung ... hanya ingin belajar ..
bhavya_w
1
@ Bhavya_w Tidak, semuanya terjadi pada satu utas. Ini adalah alasan perhitungan js yang panjang dapat memblokir UI
Seba Kerckhof
88

Lihatlah artikel John Resig tentang Cara Kerja Pengatur Waktu JavaScript . Ketika Anda mengatur batas waktu, itu sebenarnya mengantri kode asinkron sampai mesin mengeksekusi tumpukan panggilan saat ini.

Andy
sumber
23

setTimeout() memberi Anda waktu hingga elemen DOM dimuat, meskipun diatur ke 0.

Lihat ini: setTimeout

Jose Basilio
sumber
23

Sebagian besar browser memiliki proses yang disebut utas utama, yang bertanggung jawab untuk menjalankan beberapa tugas JavaScript, pembaruan UI, misalnya: melukis, menggambar ulang atau merefow, dll.

Beberapa eksekusi JavaScript dan tugas pembaruan UI diantrekan ke antrian pesan browser, kemudian dikirim ke utas utama browser untuk dieksekusi.

Ketika pembaruan UI dihasilkan saat utas utama sedang sibuk, tugas ditambahkan ke dalam antrian pesan.

setTimeout(fn, 0);tambahkan ini fnke akhir antrian yang akan dieksekusi. Ini menjadwalkan tugas yang akan ditambahkan pada antrian pesan setelah jumlah waktu tertentu.

arley
sumber
"Setiap eksekusi JavaScript dan tugas pembaruan UI ditambahkan ke sistem antrian acara browser, maka tugas-tugas itu dikirim ke browser UI Thread utama untuk dilakukan." .... sumber tolong?
bhavya_w
4
JavaScript Kinerja Tinggi (Nicholas Zakas, Stoyan Stefanov, Ross Harmes, Julien Lecomte, dan Matt Sweeney)
Arley
Downvote untuk ini add this fn to the end of the queue. Yang terpenting adalah di mana tepatnya setTimeoutmenambahkan fungsi ini, akhir dari siklus loop ini atau sangat awal siklus loop berikutnya.
Hijau
19

Ada jawaban terbalik yang bertentangan di sini, dan tanpa bukti tidak ada cara untuk mengetahui siapa yang harus dipercaya. Inilah bukti bahwa @DVK benar dan @SalvadorDali salah. Klaim terakhir:

"Dan inilah sebabnya: tidak mungkin memiliki setTimeout dengan penundaan waktu 0 milidetik. Nilai minimum ditentukan oleh browser dan bukan 0 milidetik. Secara historis browser menetapkan minimum ini menjadi 10 milidetik, tetapi spesifikasi HTML5 dan browser modern menetapkannya pada 4 milidetik. "

Batas waktu minimum 4 ms tidak relevan dengan apa yang terjadi. Apa yang sebenarnya terjadi adalah setTimeout mendorong fungsi callback ke akhir antrian eksekusi. Jika setelah setTimeout (panggilan balik, 0) Anda memiliki kode pemblokiran yang membutuhkan beberapa detik untuk dijalankan, panggilan balik tidak akan dieksekusi selama beberapa detik, sampai kode pemblokiran selesai. Coba kode ini:

function testSettimeout0 () {
    var startTime = new Date().getTime()
    console.log('setting timeout 0 callback at ' +sinceStart())
    setTimeout(function(){
        console.log('in timeout callback at ' +sinceStart())
    }, 0)
    console.log('starting blocking loop at ' +sinceStart())
    while (sinceStart() < 3000) {
        continue
    }
    console.log('blocking loop ended at ' +sinceStart())
    return // functions below
    function sinceStart () {
        return new Date().getTime() - startTime
    } // sinceStart
} // testSettimeout0

Output adalah:

setting timeout 0 callback at 0
starting blocking loop at 5
blocking loop ended at 3000
in timeout callback at 3033
Vladimir Kornea
sumber
jawaban Anda tidak membuktikan apa-apa. Itu hanya menunjukkan bahwa pada mesin Anda di bawah dalam situasi tertentu komputer melempar Anda beberapa angka. Untuk membuktikan sesuatu yang berhubungan Anda perlu sedikit lebih dari beberapa baris kode dan beberapa angka.
Salvador Dali
6
@SalvadorDali, saya percaya bukti saya cukup jelas bagi kebanyakan orang untuk mengerti. Saya pikir Anda merasa defensif dan belum berusaha untuk memahaminya. Saya akan dengan senang hati berusaha menjelaskannya, tetapi saya tidak tahu apa yang Anda tidak mengerti. Coba jalankan kode pada mesin Anda sendiri jika Anda mencurigai hasil saya.
Vladimir Kornea
Saya akan mencoba memperjelas untuk terakhir kalinya. Jawaban saya adalah mengklarifikasi beberapa properti menarik dalam batas waktu, interval dan didukung dengan baik oleh sumber yang valid dan dapat dikenali. Anda membuat klaim kuat bahwa itu salah dan menunjuk ke bagian kode Anda yang karena alasan tertentu Anda menyebutkan bukti. Tidak ada yang salah dengan bagian kode Anda (saya tidak menentangnya). Saya hanya mengatakan bahwa jawaban saya tidak salah. Ini menjelaskan beberapa poin. Saya akan menghentikan perang ini, karena saya tidak bisa melihat titik.
Salvador Dali
15

Salah satu alasan untuk melakukan itu adalah untuk menunda eksekusi kode ke loop acara berikutnya yang terpisah. Ketika menanggapi acara browser dari beberapa jenis (klik mouse, misalnya), kadang-kadang perlu untuk melakukan operasi hanya setelah acara saat ini diproses. The setTimeout()fasilitas adalah cara paling sederhana untuk melakukannya.

sunting sekarang karena tahun 2015 saya harus mencatat bahwa ada juga requestAnimationFrame(), yang tidak persis sama tetapi cukup dekat setTimeout(fn, 0)sehingga layak disebut.

Runcing
sumber
Inilah tepatnya salah satu tempat di mana saya pernah melihatnya digunakan. =)
Zaibot
FYI: jawaban ini digabungkan di sini dari stackoverflow.com/questions/4574940/...
Shog9
2
adalah untuk menunda eksekusi kode ke loop acara berikutnya yang terpisah : bagaimana Anda mengetahui loop acara berikutnya ? Bagaimana Anda mengetahui apa itu loop acara saat ini? Bagaimana Anda tahu dalam perulangan acara apa Anda sekarang?
Hijau
@Baiklah, Anda tidak, sungguh; benar-benar tidak ada visibilitas langsung ke dalam apa yang JavaScript runtime lakukan.
Runcing
requestAnimationFrame menyelesaikan masalah yang saya alami dengan IE dan Firefox kadang
David
9

Ini adalah pertanyaan lama dengan jawaban lama. Saya ingin menambahkan tampilan baru pada masalah ini dan untuk menjawab mengapa ini terjadi dan bukan mengapa ini berguna.

Jadi, Anda memiliki dua fungsi:

var f1 = function () {    
   setTimeout(function(){
      console.log("f1", "First function call...");
   }, 0);
};

var f2 = function () {
    console.log("f2", "Second call...");
};

dan kemudian memanggil mereka dalam urutan berikut f1(); f2();hanya untuk melihat bahwa yang kedua dieksekusi terlebih dahulu.

Dan inilah alasannya: tidak mungkin untuk memiliki setTimeoutwaktu tunda 0 milidetik. Nilai minimum ditentukan oleh browser dan bukan 0 milidetik. Secara historis browser menetapkan ini minimum hingga 10 milidetik, tetapi spesifikasi HTML5 dan browser modern ditetapkan pada 4 milidetik.

Jika level bersarang lebih besar dari 5, dan batas waktu kurang dari 4, maka tambah batas waktu menjadi 4.

Juga dari mozilla:

Untuk menerapkan batas waktu 0 ms dalam peramban modern, Anda dapat menggunakan window.postMessage () seperti yang dijelaskan di sini .

Informasi PS diambil setelah membaca artikel berikut .

Salvador Dali
sumber
1
@ user2407309 Apakah Anda bercanda? Maksud Anda spesifikasi HTML5 salah dan Anda benar? Baca sumber sebelum Anda downvote dan buat klaim kuat. Jawaban saya didasarkan pada spesifikasi HTML, dan catatan sejarah. Alih-alih melakukan jawaban yang menjelaskan hal-hal yang sama sekali sama berulang-ulang, saya menambahkan sesuatu yang baru, sesuatu yang tidak diperlihatkan dalam jawaban sebelumnya. Saya tidak mengatakan bahwa ini adalah satu-satunya alasan, saya hanya menunjukkan sesuatu yang baru.
Salvador Dali
1
Ini tidak benar: "Dan inilah sebabnya: tidak mungkin memiliki setTimeout dengan penundaan waktu 0 milidetik." Bukan itu sebabnya. Penundaan 4ms tidak relevan untuk mengapa setTimeout(fn,0)bermanfaat.
Vladimir Kornea
@ user2407309 dapat dengan mudah diubah menjadi "untuk menambah alasan yang dinyatakan oleh orang lain, itu tidak mungkin ....". Jadi, sangat konyol untuk melakukan downvote hanya karena ini terutama jika jawaban Anda sendiri tidak mengatakan sesuatu yang baru. Cukup edit kecil saja sudah cukup.
Salvador Dali
10
Salvador Dali: Jika Anda mengabaikan aspek emosional perang nyala api mikro di sini, Anda mungkin harus mengakui bahwa @VladimirKornea benar. Memang benar bahwa browser memetakan penundaan 0ms ke 4ms, tetapi bahkan jika mereka tidak, hasilnya akan tetap sama. Mekanisme mengemudi di sini adalah bahwa kode didorong ke antrian, daripada tumpukan panggilan. Lihat presentasi JSConf yang luar biasa ini, mungkin membantu untuk memperjelas masalah ini: youtube.com/watch?v=8aGhZQkoFbQ
hashchange
1
Saya bingung mengapa menurut Anda penawaran Anda untuk minimum yang memenuhi syarat 4 ms adalah minimum global 4 ms. Seperti yang ditunjukkan oleh kutipan Anda dari HTML5 spec, minimumnya adalah 4 ms hanya ketika Anda membuat panggilan bersarang ke setTimeout/ setIntervallebih dari lima level; jika belum, minimum adalah 0 ms (apa dengan mesin waktu kurang). Dokumen Mozilla memperluas bahwa untuk mencakup kasus yang berulang, bukan hanya bersarang (jadi setIntervaldengan interval 0 akan menjadwal ulang segera beberapa kali, kemudian menunda lebih lama setelah itu), tetapi penggunaan sederhana setTimeoutdengan sarang minimal diizinkan untuk segera mengantre.
ShadowRanger
8

Karena sedang melewati durasi 0, saya kira itu untuk menghapus kode yang diteruskan ke setTimeoutdari aliran eksekusi. Jadi jika itu adalah fungsi yang bisa memakan waktu cukup lama, itu tidak akan mencegah eksekusi kode selanjutnya.

pengguna113716
sumber
FYI: jawaban ini digabungkan di sini dari stackoverflow.com/questions/4574940/...
Shog9
7

Kedua jawaban teratas ini salah. Lihatlah deskripsi MDN pada model concurrency dan loop acara , dan akan menjadi jelas apa yang terjadi (sumber daya MDN adalah permata nyata). Dan hanya menggunakan setTimeout dapat menambahkan masalah yang tidak terduga dalam kode Anda selain "menyelesaikan" masalah kecil ini.

Apa sebenarnya terjadi di sini bukanlah bahwa "browser mungkin belum siap karena konkurensi," atau sesuatu yang berdasarkan pada "setiap baris adalah peristiwa yang ditambahkan ke bagian belakang antrian".

The jsfiddle disediakan oleh DVK memang menggambarkan masalah, tapi penjelasannya untuk itu tidak benar.

Apa yang terjadi dalam kodenya adalah bahwa dia pertama kali memasang pengendali acara ke clickacara tersebut di#do tombol.

Kemudian, ketika Anda benar-benar mengklik tombol, a messagedibuat merujuk fungsi event handler, yang akan ditambahkan ke message queue. Ketika event loopmencapai pesan ini, itu menciptakan aframe on the stack, dengan pemanggilan fungsi ke pengendali event klik di jsfiddle.

Dan di sinilah mulai menarik. Kami sudah terbiasa menganggap Javascript sebagai tidak sinkron sehingga kami cenderung mengabaikan fakta kecil ini: Setiap frame harus dieksekusi, secara penuh, sebelum frame berikutnya dapat dieksekusi . Tidak ada konkurensi, teman.

Apa artinya ini? Ini berarti bahwa setiap kali suatu fungsi dipanggil dari antrian pesan, ia memblokir antrian sampai tumpukan yang dihasilkannya dikosongkan. Atau, dalam istilah yang lebih umum, itu memblokir sampai fungsi telah kembali. Dan itu memblokir segalanya , termasuk operasi rendering DOM, scrolling, dan yang lainnya. Jika Anda ingin konfirmasi, coba saja tambahkan durasi operasi yang berjalan lama di biola (mis. Jalankan loop luar 10 kali lebih banyak), dan Anda akan melihat bahwa ketika sedang berjalan, Anda tidak dapat menggulir halaman. Jika berjalan cukup lama, browser Anda akan menanyakan apakah Anda ingin mematikan prosesnya, karena itu membuat halaman menjadi tidak responsif. Frame sedang dieksekusi, dan loop acara dan antrian pesan macet sampai selesai.

Jadi mengapa efek samping teks ini tidak diperbarui? Karena ketika Anda telah mengubah nilai elemen dalam DOM - Anda dapat console.log()nilainya segera setelah mengubahnya dan melihat bahwa itu telah diubah (yang menunjukkan mengapa penjelasan DVK tidak benar) - browser sedang menunggu tumpukan untuk menguras (ituon fungsi penangan untuk kembali) dan dengan demikian pesan untuk selesai, sehingga pada akhirnya bisa menyelesaikan mengeksekusi pesan yang telah ditambahkan oleh runtime sebagai reaksi terhadap operasi mutasi kami, dan untuk mencerminkan mutasi itu di UI .

Ini karena kami sebenarnya menunggu kode selesai berjalan. Kami belum mengatakan "seseorang mengambil ini dan kemudian memanggil fungsi ini dengan hasilnya, terima kasih, dan sekarang saya sudah selesai imma kembali, lakukan apa pun sekarang," seperti yang biasa kita lakukan dengan Javascript asinkron berbasis acara kami. Kami memasukkan fungsi event handler klik, kami memperbarui elemen DOM, kami memanggil fungsi lain, fungsi lainnya bekerja untuk waktu yang lama dan kemudian kembali, kami kemudian memperbarui elemen DOM yang sama, dan kemudian kami kembali dari fungsi awal, secara efektif mengosongkan tumpukan. Dan kemudian browser dapat membuka pesan berikutnya dalam antrian, yang mungkin merupakan pesan yang dihasilkan oleh kami dengan memicu beberapa jenis acara "on-DOM-mutation" internal.

UI browser tidak dapat (atau memilih untuk tidak) memperbarui UI sampai frame yang sedang dieksekusi selesai (fungsi telah kembali). Secara pribadi, saya pikir ini bukan dengan desain daripada pembatasan.

Mengapa setTimeouthal itu berhasil? Ia melakukannya, karena ia secara efektif menghilangkan panggilan ke fungsi yang sudah berjalan lama dari kerangkanya sendiri, menjadwalkannya untuk dieksekusi nanti dalam windowkonteksnya, sehingga ia sendiri dapat segera kembali dan memungkinkan antrian pesan untuk memproses pesan lainnya. Dan idenya adalah bahwa pesan "saat pembaruan" UI yang telah dipicu oleh kami di Javascript saat mengubah teks di DOM sekarang berada di depan pesan yang antri untuk fungsi jangka panjang, sehingga pembaruan UI terjadi sebelum kami memblokir untuk waktu yang lama.

Perhatikan bahwa a) Fungsi yang berjalan lama masih memblokir semua saat dijalankan, dan b) Anda tidak dijamin bahwa pembaruan UI sebenarnya ada di depannya dalam antrian pesan. Pada browser Chrome Juni 2018 saya, nilai 0tidak "memperbaiki" masalah yang ditunjukkan oleh biola - 10. Saya sebenarnya agak terhalang oleh ini, karena menurut saya logis bahwa pesan pembaruan UI harus di-antri sebelum itu, karena pemicunya dieksekusi sebelum penjadwalan fungsi yang berjalan lama untuk dijalankan "nanti". Tapi mungkin ada beberapa optimisasi di mesin V8 yang dapat mengganggu, atau mungkin pemahaman saya kurang.

Oke, jadi apa masalahnya dengan menggunakan setTimeout, dan apa solusi yang lebih baik untuk kasus khusus ini?

Pertama, masalah dengan menggunakan setTimeoutevent handler seperti ini, untuk mencoba mengatasi masalah lain, cenderung berantakan dengan kode lain. Ini contoh nyata dari pekerjaan saya:

Seorang kolega, dalam pemahaman yang salah informasi pada loop acara, mencoba "mengaitkan" Javascript dengan meminta beberapa kode rendering templat digunakan setTimeout 0untuk renderingnya. Dia tidak lagi di sini untuk bertanya, tetapi saya dapat berasumsi bahwa mungkin dia memasukkan timer untuk mengukur kecepatan rendering (yang akan menjadi kedekatan kembali fungsi) dan menemukan bahwa menggunakan pendekatan ini akan membuat respons yang sangat cepat dari fungsi itu.

Masalah pertama sudah jelas; Anda tidak dapat mengaitkan javascript, jadi Anda tidak memenangkan apa pun di sini saat Anda menambahkan kebingungan. Kedua, Anda sekarang telah secara efektif memisahkan rendering templat dari tumpukan kemungkinan pendengar acara yang mungkin berharap bahwa templat yang telah dirender, meskipun mungkin tidak. Perilaku aktual dari fungsi itu sekarang non-deterministik, seperti - tanpa sadar begitu - fungsi apa pun yang akan menjalankannya, atau bergantung padanya. Anda dapat membuat tebakan yang terpelajar, tetapi Anda tidak dapat membuat kode yang benar untuk perilakunya.

"Memperbaiki" saat menulis event handler baru yang bergantung pada logikanya juga digunakan setTimeout 0. Tapi, itu bukan perbaikan, sulit untuk dipahami, dan tidak menyenangkan untuk men-debug kesalahan yang disebabkan oleh kode seperti ini. Kadang-kadang tidak ada masalah, di lain waktu itu gagal secara konsisten, dan sekali lagi, kadang-kadang ia bekerja dan rusak secara sporadis, tergantung pada kinerja platform saat ini dan apa pun yang terjadi pada saat itu. Inilah mengapa saya pribadi menyarankan untuk tidak menggunakan hack ini (ini adalah hack, dan kita semua harus tahu itu), kecuali Anda benar-benar tahu apa yang Anda lakukan dan apa konsekuensinya.

Tapi apa yang bisa kita lakukan? Yah, seperti yang disarankan oleh artikel MDN yang direferensikan, pisahkan pekerjaan menjadi beberapa pesan (jika Anda bisa) sehingga pesan lain yang di-antri dapat disatukan dengan pekerjaan Anda dan dieksekusi saat dijalankan, atau menggunakan pekerja web, yang dapat menjalankan bersama-sama dengan halaman Anda dan mengembalikan hasil ketika dilakukan dengan perhitungannya.

Oh, dan jika Anda berpikir, "Yah, tidak bisakah saya melakukan panggilan balik pada fungsi yang sudah berjalan lama untuk menjadikannya tidak sinkron ?," lalu tidak. Callback tidak menjadikannya tidak sinkron, itu masih harus menjalankan kode yang sudah berjalan lama sebelum secara eksplisit memanggil panggilan balik Anda.

DanielSmedegaardBuus
sumber
3

Hal lain yang dilakukan adalah mendorong pemanggilan fungsi ke bagian bawah tumpukan, mencegah tumpukan meluap jika Anda secara berulang memanggil suatu fungsi. Ini memiliki efek whileloop tetapi membiarkan mesin JavaScript menyalakan timer asinkron lainnya.

Jason Suárez
sumber
Downvote untuk ini push the function invocation to the bottom of the stack. Apa stackyang Anda bicarakan tidak jelas. Yang terpenting adalah di mana tepatnya setTimeoutmenambahkan fungsi ini, akhir dari siklus loop ini atau sangat awal siklus loop berikutnya.
Hijau
1

Dengan memanggil setTimeout Anda memberi waktu pada halaman untuk bereaksi terhadap apa pun yang dilakukan pengguna. Ini sangat membantu untuk fungsi yang dijalankan selama pemuatan halaman.

Jeremy
sumber
1

Beberapa kasus lain di mana setTimeout berguna:

Anda ingin memecah loop yang sudah berjalan lama atau perhitungan menjadi komponen yang lebih kecil sehingga browser tidak tampak 'membeku' atau mengatakan "Script pada halaman sedang sibuk".

Anda ingin menonaktifkan tombol kirim formulir ketika diklik, tetapi jika Anda menonaktifkan tombol di pengendali onClick formulir tidak akan dikirimkan. setTimeout dengan waktu nol berfungsi, membiarkan acara berakhir, formulir untuk mulai mengirimkan, lalu tombol Anda dapat dinonaktifkan.

fabspro
sumber
2
Menonaktifkan akan lebih baik dilakukan dalam acara onsubmit; itu akan lebih cepat dan dijamin akan dipanggil sebelum formulir diserahkan secara teknis karena Anda dapat menghentikan pengiriman.
Kris
Sangat benar. Saya kira penonaktifan onclick lebih mudah untuk prototyping karena Anda cukup mengetik onclick = "this.disabled = true" pada tombol sedangkan penonaktifan pada pengiriman memerlukan sedikit lebih banyak pekerjaan.
fabspro
1

Jawaban tentang loop eksekusi dan rendering DOM sebelum beberapa kode lainnya selesai benar. Nol timeout kedua dalam JavaScript membantu membuat kode pseudo-multithreaded, meskipun tidak.

Saya ingin menambahkan bahwa nilai TERBAIK untuk lintas peramban / lintas platform batas waktu nol detik dalam JavaScript sebenarnya sekitar 20 milidetik alih-alih 0 (nol), karena banyak peramban seluler tidak dapat mendaftarkan batas waktu lebih kecil dari 20 milidetik karena keterbatasan jam pada chip AMD.

Selain itu, proses jangka panjang yang tidak melibatkan manipulasi DOM harus dikirim ke Pekerja Web sekarang, karena mereka memberikan eksekusi benar-benar multithreaded JavaScript.

ChrisN
sumber
2
Saya agak skeptis tentang jawaban Anda, tetapi membatalkannya karena memaksa saya untuk melakukan riset tambahan tentang standar browser. Ketika meneliti standar, saya pergi ke tempat saya selalu pergi, MDN: developer.mozilla.org/en-US/docs/Web/API/window.setTimeout spec HTML5 mengatakan 4ms. Itu tidak mengatakan apa-apa tentang batasan jam pada chip ponsel. Memiliki kesulitan menelusuri Google untuk sumber info untuk membuat cadangan pernyataan Anda. Apakah mengetahui Bahasa Dart oleh Google menghapus setTimeout sama sekali demi objek Timer.
John Zabroski
8
(...) karena banyak peramban seluler tidak dapat mendaftarkan batas waktu lebih kecil dari 20 milidetik karena batasan jam (...) Setiap platform memiliki batasan waktu karena jamnya dan tidak ada platform yang mampu mengeksekusi hal berikutnya tepat 0 ms setelah yang sekarang. Waktu habis 0ms meminta eksekusi fungsi sesegera mungkin dan batasan waktu platform tertentu tidak mengubah arti ini dengan cara apa pun.
Piotr Dobrogost
1

setTimout pada 0 juga sangat berguna dalam pola pengaturan janji yang ditangguhkan, yang ingin segera Anda kembalikan:

myObject.prototype.myMethodDeferred = function() {
    var deferredObject = $.Deferred();
    var that = this;  // Because setTimeout won't work right with this
    setTimeout(function() { 
        return myMethodActualWork.call(that, deferredObject);
    }, 0);
    return deferredObject.promise();
}
Stephan G
sumber
1

Masalahnya adalah Anda mencoba melakukan operasi Javascript pada elemen yang tidak ada. Elemen belum dimuat dan setTimeout()memberi lebih banyak waktu bagi elemen untuk memuat dengan cara berikut:

  1. setTimeout()menyebabkan acara menjadi ansinkron karena itu dijalankan setelah semua kode sinkron, memberikan elemen Anda lebih banyak waktu untuk memuat. Panggilan balik yang tidak sinkron seperti panggilan balik setTimeout()ditempatkan di antrian acara dan diletakkan di tumpukan oleh loop acara sinkron setelah tumpukan kode sinkron kosong.
  2. Nilai 0 untuk ms sebagai argumen kedua dalam fungsi setTimeout()seringkali sedikit lebih tinggi (4-10 ms tergantung pada browser). Waktu yang sedikit lebih tinggi ini diperlukan untuk menjalankan setTimeout()callback disebabkan oleh jumlah 'ticks' (di mana sebuah tick mendorong callback pada stack jika stack kosong) dari loop acara. Karena alasan kinerja dan usia baterai, jumlah kutu dalam loop aktivitas dibatasi hingga jumlah tertentu kurang dari 1000 kali per detik.
Willem van der Veen
sumber
-1

Javascript adalah aplikasi berulir tunggal sehingga tidak memungkinkan untuk menjalankan fungsi secara bersamaan sehingga untuk mencapai hal ini loop digunakan. Jadi persis apa yang dilakukan setTimeout (fn, 0) dengan tugas pencarian yang dijalankan ketika tumpukan panggilan Anda kosong. Saya tahu penjelasan ini cukup membosankan, jadi saya sarankan Anda membaca video ini karena ini akan membantu Anda bagaimana berbagai hal bekerja di bawah kap browser. Lihat video ini: - https://www.youtube.com/watch?time_continue=392&v=8aGhZQkoFbQ

Jani Devang
sumber