1. Mengapa JavaScript Single-Threaded?
Fitur utama dari bahasa JavaScript adalah threading tunggal, yang berarti Anda hanya dapat melakukan satu hal secara bersamaan. Jadi, mengapa JavaScript tidak dapat memiliki banyak utas? Ini akan meningkatkan efisiensi.
Threading tunggal Javascript terkait dengan tujuannya. Sebagai bahasa skrip browser, tujuan utama JavaScript adalah untuk berinteraksi dengan pengguna dan mengoperasikan DOM. Ini menentukan bahwa itu hanya dapat berulir tunggal, jika tidak, ia akan menyebabkan masalah sinkronisasi yang sangat kompleks. Misalnya, misalkan JavaScript memiliki dua utas secara bersamaan, satu utas menambahkan konten pada simpul DOM tertentu, dan utas lainnya menghapus node ini, utas mana yang harus diambil browser saat ini?
Oleh karena itu, untuk menghindari kompleksitas, JavaScript adalah satu utas dari kelahirannya, yang telah menjadi fitur inti dari bahasa ini dan tidak akan berubah di masa depan.
Untuk memanfaatkan kekuatan komputasi CPU multi-core, HTML5 mengusulkan standar pekerja web, memungkinkan skrip JavaScript untuk membuat banyak utas, tetapi utas anak sepenuhnya dikendalikan oleh utas utama dan tidak dapat mengoperasikan DOM. Oleh karena itu, standar baru ini tidak mengubah sifat threading tunggal JavaScript.
2. Antrian tugas
Single Threading berarti bahwa semua tugas perlu antri, dan tugas sebelumnya akan dieksekusi sebelum tugas berikutnya akan dieksekusi. Jika tugas sebelumnya membutuhkan waktu lama, tugas selanjutnya harus menunggu.
Jika antriannya karena sejumlah besar komputasi dan CPU terlalu sibuk, itu akan baik -baik saja, tetapi berkali -kali CPU menganggur karena perangkat IO (perangkat input dan output) sangat lambat (seperti operasi Ajax membaca data dari jaringan), dan Anda harus menunggu hasilnya keluar sebelum melaksanakannya.
Perancang bahasa JavaScript menyadari bahwa pada saat ini, CPU sepenuhnya dapat mengabaikan perangkat IO, menangguhkan tugas menunggu dan menjalankan tugas berikutnya terlebih dahulu. Tunggu sampai perangkat IO mengembalikan hasilnya, lalu putar dan lanjutkan tugas yang ditangguhkan.
Oleh karena itu, JavaScript memiliki dua metode eksekusi: satu adalah bahwa CPU dieksekusi secara berurutan, tugas sebelumnya berakhir, dan kemudian tugas berikutnya dieksekusi, yang disebut eksekusi sinkron; Yang lainnya adalah bahwa CPU melewatkan tugas dengan waktu tunggu yang lama dan memproses tugas -tugas berikutnya terlebih dahulu, yang disebut eksekusi asinkron. Pemrogram memilih secara mandiri metode eksekusi seperti apa untuk diadopsi.
Secara khusus, mekanisme operasi eksekusi asinkron adalah sebagai berikut. (Hal yang sama berlaku untuk eksekusi sinkron, karena dapat dianggap sebagai eksekusi asinkron tanpa tugas asinkron.)
(1) Semua tugas dieksekusi di utas utama untuk membentuk tumpukan konteks eksekusi.
(2) Selain utas utama, ada juga "antrian tugas". Sistem menempatkan tugas asinkron ke dalam "antrian tugas" dan kemudian terus melaksanakan tugas -tugas berikutnya.
(3) Setelah semua tugas dalam "tumpukan eksekusi" dieksekusi, sistem akan membaca "antrian tugas". Jika pada saat ini, tugas asinkron telah mengakhiri keadaan menunggu, itu akan memasuki tumpukan eksekusi dari "antrian tugas" dan melanjutkan eksekusi.
(4) Utas utama terus mengulangi langkah ketiga di atas.
Gambar berikut adalah diagram skematik dari utas utama dan antrian tugas.
Selama utas utama kosong, ia akan membaca "antrian tugas". Ini adalah mekanisme berjalan JavaScript. Proses ini akan diulang terus menerus.
3. Acara dan fungsi panggilan balik
"Tugas antrian" pada dasarnya adalah antrian peristiwa (juga dipahami sebagai antrian pesan). Ketika perangkat IO menyelesaikan tugas, itu menambahkan acara ke "antrian tugas", menunjukkan bahwa tugas asinkron yang relevan dapat memasukkan "Eksekusi Stack". Utas utama membaca "antrian tugas", yang berarti membaca peristiwa apa yang ada di dalamnya.
Acara dalam "antrian tugas" termasuk acara di samping acara dari perangkat IO, tetapi juga acara yang dihasilkan oleh pengguna (seperti klik mouse, pengguliran halaman, dll.). Selama fungsi callback ditentukan, peristiwa ini akan memasuki "antrian tugas" ketika mereka terjadi dan menunggu utas utama dibaca.
Yang disebut "panggilan balik" adalah kode yang akan digantung oleh utas utama. Tugas asinkron harus menentukan fungsi panggilan balik. Ketika tugas asinkron kembali dari "antrian tugas" ke tumpukan eksekusi, fungsi callback akan dieksekusi.
"Tugas Antrian" adalah struktur data pertama-dalam pertama, dengan peristiwa yang diperingkat pertama dan lebih disukai untuk kembali ke utas utama. Proses bacaan utas utama pada dasarnya otomatis. Selama tumpukan eksekusi dihapus, acara pertama pada "antrian tugas" akan secara otomatis kembali ke utas utama. Namun, karena fungsi "timer" yang disebutkan kemudian, utas utama perlu memeriksa waktu eksekusi, dan beberapa peristiwa harus kembali ke utas utama pada waktu yang ditentukan.
4. Loop acara
Utas utama membaca acara dari "antrian tugas". Proses ini terus -menerus, sehingga seluruh mekanisme berjalan juga disebut Loop Event.
Untuk lebih memahami Loop Acara, silakan lihat gambar di bawah ini (dikutip dari pidato Philip Roberts "Bantuan, saya terjebak dalam loop acara").
Pada gambar di atas, ketika utas utama berjalan, ia menghasilkan tumpukan dan tumpukan. Kode dalam tumpukan memanggil berbagai API eksternal, yang menambahkan berbagai peristiwa (klik, muat, selesai) ke "antrian tugas". Selama kode dalam tumpukan dieksekusi, utas utama akan membaca "antrian tugas" dan menjalankan fungsi callback yang sesuai dengan peristiwa tersebut secara bergantian.
Jalankan kode di tumpukan, selalu dieksekusi sebelum membaca "antrian tugas". Silakan lihat contoh berikut.
Salinan kode adalah sebagai berikut:
var req = new XmlHttpRequest ();
req.open ('get', url);
req.onload = function () {};
req.onError = function () {};
req.send ();
Metode Req.Send dalam kode di atas adalah operasi AJAX untuk mengirim data ke server. Ini adalah tugas yang tidak sinkron, yang berarti bahwa sistem akan membaca "antrian tugas" hanya setelah semua kode dalam skrip saat ini dieksekusi. Jadi, itu setara dengan metode penulisan berikut.
Salinan kode adalah sebagai berikut:
var req = new XmlHttpRequest ();
req.open ('get', url);
req.send ();
req.onload = function () {};
req.onError = function () {};
Dengan kata lain, bagian -bagian dari fungsi callback yang ditentukan (Onload dan OnError) tidak penting sebelum atau sesudah metode Send (), karena mereka adalah bagian dari tumpukan eksekusi, dan sistem akan selalu menjalankannya sebelum membaca "antrian tugas".
5. Timer
Selain menempatkan tugas asinkron, "antrian tugas" juga memiliki fungsi lain, yaitu untuk menempatkan acara waktunya, yaitu, menentukan berapa lama kode tertentu akan dieksekusi setelahnya. Ini disebut fungsi "timer", yang merupakan kode yang dieksekusi secara berkala.
Fungsi timer terutama diselesaikan oleh dua fungsi: setTimeout () dan setInterval (). Mekanisme berjalan internal mereka persis sama. Perbedaannya adalah bahwa kode yang ditentukan oleh yang pertama dieksekusi pada satu waktu, sedangkan yang terakhir dieksekusi berulang kali. Berikut ini terutama membahas setTimeout ().
setTimeout () menerima dua parameter, yang pertama adalah fungsi callback, dan yang kedua adalah jumlah milidetik untuk menunda eksekusi.
Salinan kode adalah sebagai berikut:
Console.log (1);
setTimeout (function () {console.log (2);}, 1000);
Console.log (3);
Hasil eksekusi dari kode di atas adalah 1, 3, 2, karena setTimeout () menunda baris kedua sampai setelah 1000 milidetik.
Jika parameter kedua dari setTimeout () diatur ke 0, itu berarti bahwa fungsi panggilan balik yang ditentukan (0 interval milidetik) dieksekusi segera setelah kode saat ini dijalankan (tumpukan eksekusi dihapus).
Salinan kode adalah sebagai berikut:
setTimeout (function () {console.log (1);}, 0);
Console.log (2);
Hasil eksekusi dari kode di atas selalu 2 dan 1, karena sistem akan menjalankan fungsi callback dalam "antrian tugas" hanya setelah baris kedua dieksekusi.
Standar HTML5 menentukan bahwa nilai minimum (interval terpendek) dari parameter kedua dari setTimeout () tidak boleh kurang dari 4 milidetik. Jika lebih rendah dari nilai ini, ia akan meningkat secara otomatis. Sebelum ini, browser yang lebih tua menetapkan interval minimum menjadi 10 milidetik.
Selain itu, untuk perubahan DOM tersebut (terutama bagian-bagian yang melibatkan rendering ulang halaman), mereka biasanya tidak segera dieksekusi, tetapi setiap 16 milidetik. Pada saat ini, efek menggunakan RequestAnimationFrame () lebih baik daripada setTimeout ().
Perlu dicatat bahwa setTimeout () hanya memasukkan acara ke dalam "antrian tugas". Anda harus menunggu sampai kode saat ini (Eksekusi Stack) dieksekusi sebelum utas utama akan menjalankan fungsi callback yang ditentukan. Jika kode saat ini membutuhkan waktu lama, mungkin perlu waktu lama untuk menunggu, jadi tidak ada jaminan bahwa fungsi panggilan balik akan dieksekusi pada saat yang ditentukan oleh setTimeout ().
6. Loop Acara Node.js
Node.js juga merupakan loop acara tunggal, tetapi mekanisme berjalannya berbeda dari lingkungan browser.
Silakan lihat diagram di bawah ini (penulis @busyrich).
Menurut gambar di atas, mekanisme berjalan Node.js adalah sebagai berikut.
(1) V8 engine parses skrip JavaScript.
(2) Kode Parsed memanggil Node API.
(3) Perpustakaan Libuv bertanggung jawab atas pelaksanaan API simpul. Ini memberikan tugas yang berbeda untuk utas yang berbeda, membentuk loop peristiwa, dan mengembalikan hasil eksekusi tugas ke mesin V8 dengan cara yang tidak sinkron.
(4) Mesin V8 mengembalikan hasilnya kepada pengguna.
Selain dua metode penyelesaian dan setInterval, Node.js juga menyediakan dua metode lain yang terkait dengan "antrian tugas": Process.nexttick dan setimmediate. Mereka dapat membantu kita memperdalam pemahaman kita tentang "antrian tugas".
Metode Process.nexttick dapat memicu fungsi panggilan balik di akhir "tumpukan eksekusi" saat ini sebelum utas utama membaca "antrian tugas" di lain waktu. Artinya, tugas yang ditentukan selalu terjadi sebelum semua tugas asinkron. Metode setimmediate memicu fungsi panggilan balik di ekor "antrian tugas" saat ini, yaitu, tugas yang ditentukan selalu dieksekusi pada saat utas utama membaca "antrian tugas", yang sangat mirip dengan SetTimeout (FN, 0). Silakan lihat contoh berikut (melalui StackOverflow).
Salinan kode adalah sebagai berikut:
process.nexttick (function a () {
Console.log (1);
process.nexttick (fungsi b () {console.log (2);});
});
setTimeout (function timeout () {
console.log ('timeout dipecat');
}, 0)
// 1
// 2
// Timeout dipecat
Dalam kode di atas, karena fungsi callback yang ditentukan oleh Metode Process.NextTick selalu dipicu pada ekor "Stack Eksekusi" saat ini, tidak hanya fungsi A dieksekusi terlebih dahulu daripada batas waktu fungsi callback yang ditentukan oleh SetTimeout, tetapi fungsi B juga dieksekusi terlebih dahulu daripada batas waktu. Ini berarti bahwa jika ada beberapa proses.nexttick pernyataan (terlepas dari apakah mereka bersarang atau tidak), mereka semua akan dieksekusi pada "tumpukan eksekusi" saat ini.
Sekarang, mari kita lihat setimmediate.
Salinan kode adalah sebagai berikut:
setimmediate (fungsi a () {
Console.log (1);
setimmediate (fungsi b () {console.log (2);});
});
setTimeout (function timeout () {
console.log ('timeout dipecat');
}, 0)
// 1
// Timeout dipecat
// 2
Dalam kode di atas, ada dua setMmediate. Setimmediate pertama menentukan bahwa fungsi callback A dipicu pada ekor "antrian tugas" saat ini ("loop peristiwa" berikutnya); Kemudian, SetTimeout juga menentukan bahwa batas waktu fungsi panggilan balik dipicu pada ekor "antrian tugas" saat ini, jadi dalam hasil output, batas waktu yang dipecat berada di belakang 1. Mengenai peringkat 2 di belakang batas waktu yang dipecat, itu karena fitur penting lain dari Setimmediate: "Loop peristiwa" hanya dapat memicu fungsi panggilan balik yang ditentukan oleh setimmiate.
Kami telah memperoleh perbedaan penting dari ini: beberapa proses. Pernyataan NextTick selalu dieksekusi sekaligus, sementara beberapa setimmediat membutuhkan beberapa kali untuk dieksekusi. Bahkan, inilah mengapa Node.js versi 10.0 menambahkan metode setimmediate. Kalau tidak, panggilan rekursif untuk memproses.nexttick seperti berikut ini tidak akan ada habisnya, dan utas utama tidak akan membaca "antrian acara" sama sekali!
Salinan kode adalah sebagai berikut:
process.nexttick (function foo () {
Process.nexttick (foo);
});
Bahkan, sekarang jika Anda menulis proses rekursif.nexttick, node.js akan melempar peringatan yang meminta Anda untuk berubah menjadi setimmediate.
Selain itu, karena fungsi callback yang ditentukan oleh Process.NextTick dipicu dalam "Loop Acara" ini dan setimmediate menetapkan bahwa itu dipicu dalam "loop peristiwa" berikutnya, jelas bahwa yang pertama selalu terjadi lebih awal daripada yang terakhir dan juga lebih efisien dalam eksekusi (karena tidak perlu memeriksa "tugas tugas").