Cara cepat memulai VUE3.0: Memulai Pembelajaran
Terkait Node.js, saya yakin sebagian besar insinyur front-end akan berpikir untuk mengembangkan server berdasarkan Node.js. Anda hanya perlu menguasai JavaScript untuk menjadi full-stack engineer, tapi sebenarnya arti dari Node.js Dan bukan itu saja.
Untuk banyak bahasa tingkat tinggi, izin eksekusi dapat mencapai sistem operasi, tetapi JavaScript yang berjalan di sisi browser merupakan pengecualian. Lingkungan kotak pasir yang dibuat oleh browser menyegel insinyur front-end di menara gading di dunia pemrograman. Namun, kemunculan Node.js telah menutupi kekurangan ini, dan para insinyur front-end juga dapat menjangkau dasar-dasar dunia komputer.
Oleh karena itu, pentingnya Nodejs bagi para insinyur front-end tidak hanya untuk menyediakan kemampuan pengembangan full-stack, namun yang lebih penting, untuk membuka pintu ke dunia komputer yang mendasarinya bagi para insinyur front-end. Artikel ini membuka pintu tersebut dengan menganalisis prinsip implementasi Node.js.
Ada lebih dari selusin dependensi di direktori /deps gudang kode sumber Node.js, termasuk modul yang ditulis dalam bahasa C (seperti libuv, V8) dan modul yang ditulis dalam bahasa JavaScript (seperti acorn , plugin acorn). Seperti yang ditunjukkan di bawah ini.
Yang paling penting adalah modul yang sesuai dengan direktori v8 dan uv. V8 sendiri tidak memiliki kemampuan untuk berjalan secara asynchronous, namun diimplementasikan dengan bantuan thread lain di browser. Inilah sebabnya kita sering mengatakan bahwa js adalah single-threaded, karena mesin parsingnya hanya mendukung kode parsing sinkron. Namun di Node.js, implementasi asinkron terutama bergantung pada libuv. Mari kita fokus menganalisis prinsip implementasi libuv.
libuv adalah pustaka I/O asinkron yang ditulis dalam C yang mendukung banyak platform. Ini terutama memecahkan masalah operasi I/O yang dengan mudah menyebabkan pemblokiran. Awalnya dikembangkan secara khusus untuk digunakan dengan Node.js, namun sejak itu telah digunakan oleh modul lain seperti Luvit, Julia, dan pyuv. Gambar di bawah adalah diagram struktur libuv.
libuv memiliki dua metode implementasi asynchronous, yaitu dua bagian yang dipilih oleh kotak kuning di kiri dan kanan gambar di atas.
Bagian kiri adalah modul I/O jaringan, yang memiliki mekanisme implementasi berbeda pada platform berbeda. Sistem Linux menggunakan epoll untuk mengimplementasikannya, OSX dan sistem BSD lainnya menggunakan KQueue, sistem SunOS menggunakan port Event, dan sistem Windows menggunakan IOCP. Karena ini melibatkan API yang mendasari sistem operasi, pemahamannya lebih rumit, jadi saya tidak akan memperkenalkannya di sini.
Bagian kanan mencakup modul file I/O, modul DNS, dan kode pengguna, yang mengimplementasikan operasi asinkron melalui kumpulan thread. I/O file berbeda dari I/O jaringan. libuv tidak bergantung pada API yang mendasari sistem, namun melakukan operasi I/O file yang diblokir di kumpulan thread global.
Gambar berikut adalah diagram alur kerja polling event yang diberikan oleh situs resmi libuv Mari kita analisa bersama dengan kodenya.
Kode inti dari loop peristiwa libuv diimplementasikan dalam fungsi uv_run(). Berikut ini adalah bagian dari kode inti di bawah sistem Unix. Meskipun ditulis dalam bahasa C, namun merupakan bahasa tingkat tinggi seperti JavaScript, sehingga tidak terlalu sulit untuk dipahami. Perbedaan terbesar mungkin terletak pada tanda bintang dan tanda panah. Kita bisa mengabaikan tanda bintang tersebut. Misalnya, loop uv_loop_t* dalam parameter fungsi dapat dipahami sebagai loop variabel bertipe uv_loop_t. Panah "→" dapat dipahami sebagai titik ".", misalnya loop→stop_flag dapat dipahami sebagai loop.stop_flag.
int uv_run(perulangan uv_loop_t*, mode uv_run_mode) { ... r = uv__loop_alive(lingkaran); jika (!r) uv__update_time(lingkaran); while (r != 0 && putaran - >stop_flag == 0) { uv__update_time(putaran); uv__run_timer(putaran); ran_pending = uv__run_pending(putaran); uv__run_idle(putaran); uv__run_prepare(loop);...uv__io_poll(loop, batas waktu); uv__run_check(putaran); uv__run_closing_handles(lingkaran);... }... }
uv__loop_alive
Fungsi ini digunakan untuk menentukan apakah polling peristiwa harus dilanjutkan. Jika tidak ada tugas aktif di objek loop, ia akan mengembalikan 0 dan keluar dari loop.
Dalam bahasa C, "tugas" ini memiliki nama profesional, yaitu "pegangan", yang dapat dipahami sebagai variabel yang menunjuk pada tugas tersebut. Pegangan dapat dibagi menjadi dua kategori: permintaan dan pegangan, yang masing-masing mewakili pegangan siklus hidup pendek dan pegangan siklus umur panjang. Kode spesifiknya adalah sebagai berikut:
static int uv__loop_alive(const uv_loop_t * loop) { kembalikan uv__has_active_handles(lingkaran) ||. uv__has_active_reqs(lingkaran) ||.lingkaran - >penutup_tangan != NULL; }
uv__update_time
Untuk mengurangi jumlah panggilan sistem terkait waktu, fungsi ini digunakan untuk menyimpan waktu sistem saat ini dalam cache. Akurasinya sangat tinggi dan dapat mencapai level nanodetik, tetapi satuannya masih milidetik.
Kode sumber spesifiknya adalah sebagai berikut:
UV_UNUSED(static void uv__update_time(uv_loop_t * loop)) { putaran - >waktu = uv__hrtime(UV_CLOCK_FAST) / 1000000; }
uv__run_timers
menjalankan fungsi callback yang mencapai ambang waktu di setTimeout() dan setInterval(). Proses eksekusi ini diimplementasikan melalui traversal loop for. Seperti yang Anda lihat dari kode di bawah ini, callback pengatur waktu disimpan dalam data struktur heap minimum. Ini keluar ketika heap minimum kosong atau belum mencapai ambang batas waktu .
Hapus pengatur waktu sebelum menjalankan fungsi panggilan balik pengatur waktu. Jika pengulangan diatur, maka perlu ditambahkan lagi ke tumpukan minimum, dan kemudian panggilan balik pengatur waktu dijalankan.
Kode spesifiknya adalah sebagai berikut:
void uv__run_timers(uv_loop_t * loop) { struct heap_node * heap_node; uv_timer_t * pegangan; untuk (;;) { heap_node = heap_min(timer_heap(lingkaran)); jika (heap_node == NULL) rusak; pegangan = container_of(heap_node, uv_timer_t, heap_node); if (handle - >timeout > loop - >time) break; uv_timer_stop(pegangan); uv_timer_again(pegangan); pegangan ->timer_cb(pegangan); } }
uv__run_pending
melintasi semua fungsi panggilan balik I/O yang disimpan di pending_queue, dan mengembalikan 0 ketika pending_queue kosong; jika tidak, mengembalikan 1 setelah menjalankan fungsi panggilan balik di pending_queue.
Kodenya sebagai berikut:
static int uv__run_pending(uv_loop_t * loop) { ANTRIAN*q; ANTRIAN pq; uv__io_t *w; if (QUEUE_EMPTY( & loop - >pending_queue)) mengembalikan 0; ANTRIAN_MOVE( & putaran - >antrian_tertunda, &pq); while (!QUEUE_EMPTY( & pq)) { q = ANTRIAN_HEAD( & pq); ANTRIAN_HAPUS(q); ANTRIAN_INIT(q); w = ANTRIAN_DATA(q, uv__io_t, antrian_tertunda); w - >cb(lingkaran, w, POLLOUT); } kembali 1; }Ketiga fungsi
uvrun_idle / uvrun_prepare / uv__run_check
semuanya didefinisikan melalui fungsi makro UV_LOOP_WATCHER_DEFINE. Fungsi makro dapat dipahami sebagai templat kode, atau fungsi yang digunakan untuk mendefinisikan fungsi. Fungsi makro dipanggil tiga kali dan nilai parameter nama prepare, check, dan idle masing-masing diteruskan. Pada saat yang sama, tiga fungsi, uvrun_idle, uvrun_prepare, dan uv__run_check, ditentukan.
Oleh karena itu, logika eksekusinya konsisten. Mereka semua mengulang dan mengeluarkan objek dalam antrian loop->name##_handles sesuai dengan prinsip masuk pertama, keluar pertama, dan kemudian menjalankan fungsi panggilan balik yang sesuai.
#define UV_LOOP_WATCHER_DEFINE(nama, jenis) batal uv__run_##nama(uv_loop_t* putaran) { uv_##nama##_t* h; antrian ANTRIAN; ANTRIAN* q; ANTRIAN_MOVE(&loop->nama##_pegangan, &antrian); while (!QUEUE_EMPTY(&antrian)) { q = ANTRIAN_HEAD(&antrian); h = ANTRIAN_DATA(q, uv_##nama##_t, antrian); ANTRIAN_HAPUS(q); QUEUE_INSERT_TAIL(&loop->nama##_handles, q); h->nama##_cb(h); } } UV_LOOP_WATCHER_DEFINE(persiapkan, PERSIAPKAN) UV_LOOP_WATCHER_DEFINE(periksa, PERIKSA) UV_LOOP_WATCHER_DEFINE(idle, IDLE)
uv__io_poll
uv__io_poll terutama digunakan untuk melakukan polling pada operasi I/O. Implementasi spesifiknya akan bervariasi tergantung pada sistem operasinya. Kami mengambil sistem Linux sebagai contoh untuk analisis.
Fungsi uv__io_poll memiliki banyak kode sumber. Intinya adalah dua buah kode loop. Bagian kodenya adalah sebagai berikut:
void uv__io_poll(uv_loop_t * loop, int timeout) { while (!QUEUE_EMPTY( & loop - >watcher_queue)) { q = QUEUE_HEAD( & putaran - >watcher_queue); ANTRIAN_HAPUS(q); ANTRIAN_INIT(q); w = ANTRIAN_DATA(q, uv__io_t, antrian_pengamat); e.events = w ->pevents; e.data.fd = w ->fd; jika (w - >peristiwa == 0) op = EPOLL_CTL_ADD; lain op = EPOLL_CTL_MOD; if (epoll_ctl(loop - >backend_fd, op, w - >fd, &e)) { if (errno != EEXIST) batalkan(); if (epoll_ctl(loop - >backend_fd, EPOLL_CTL_MOD, w - >fd, &e)) batalkan(); } w - >peristiwa = w - >peristiwa; } untuk (;;) { untuk (saya = 0; saya < nfds; saya++) { pe = kejadian + i; fd = pe ->data.fd; w = loop - >pengamat[fd]; pe - >acara &= w - >peacara |. if (pe - >events == POLLERR || pe - >events == POLLHUP) pe - >events |= w - >pevents & (POLLIN | POLLOUT | UV__POLLRDHUP | UV__POLLPRI); jika (pe - >peristiwa != 0) { if (w == &loop - >signal_io_watcher) punya_sinyal = 1; lain w - >cb(loop, w, pe - >events); tidak ada acara++; } } if (memiliki sinyal != 0) loop - >signal_io_watcher.cb(loop, &loop - >signal_io_watcher, POLLIN); }... }
Dalam perulangan while, lintasi antrian pengamat watcher_queue, keluarkan event dan deskriptor file dan tetapkan ke objek event e, lalu panggil fungsi epoll_ctl untuk mendaftarkan atau memodifikasi event epoll.
Dalam loop for, deskriptor file yang menunggu di epoll akan dikeluarkan dan ditugaskan ke nfds, dan kemudian nfds akan dilintasi untuk menjalankan fungsi panggilan balik.
uv__run_closing_handles
melintasi antrian yang menunggu untuk ditutup, menutup pegangan seperti stream, tcp, udp, dll., lalu memanggil close_cb yang sesuai dengan pegangan tersebut. Kodenya sebagai berikut:
static void uv__run_closing_handles(uv_loop_t * loop) { uv_handle_t * p; uv_handle_t * q; p = putaran ->penutup_pegangan; lingkaran ->closing_handles = NULL; sementara (p) { q = p ->next_closing; uv__finish_close(p); hal = q; } }
Meskipun process.nextTick dan Promise keduanya merupakan API asinkron, keduanya bukan bagian dari jajak pendapat peristiwa. Mereka memiliki antrean tugasnya sendiri, yang dijalankan setelah setiap langkah jajak pendapat peristiwa selesai. Jadi ketika kita menggunakan dua API asinkron ini, kita perlu memperhatikan. Jika tugas panjang atau rekursi dilakukan dalam fungsi panggilan balik yang masuk, polling peristiwa akan diblokir, sehingga operasi I/O "kelaparan".
Kode berikut adalah contoh di mana fungsi panggilan balik fs.readFile tidak dapat dijalankan dengan memanggil prcoess.nextTick secara rekursif.
fs.readFile('config.json', (err, data) = >{... }) lintasan konstan = () = >{ proses.nextTick(melintasi) }
Untuk mengatasi masalah ini, Anda dapat menggunakan setImmediate, karena setImmediate akan mengeksekusi antrian fungsi panggilan balik di loop peristiwa. Antrean tugas process.nextTick memiliki prioritas lebih tinggi daripada antrean tugas Promise. Untuk alasan spesifik, lihat kode berikut:
function processTicksAndRejections() { biarkan tock; Mengerjakan { while (tock = antrian.shift()) { const asyncId = tock[async_id_symbol]; emitBefore(asyncId, tock[trigger_async_id_symbol], tock); mencoba { const panggilan balik = tock.panggilan balik; if (tock.args === tidak terdefinisi) { panggilan balik(); } kalau tidak { const args = tock.args; beralih (args.panjang) { kasus 1: panggilan balik(args[0]); merusak; kasus 2: panggilan balik(args[0], args[1]); merusak; kasus 3: panggilan balik(args[0], args[1], args[2]); merusak; kasus 4: panggilan balik(args[0], args[1], args[2], args[3]); merusak; bawaan: panggilan balik(...args); } } } Akhirnya { if (destroyHooksExist()) emitDestroy(asyncId); } emitAfter(asyncId); } jalankanMicrotasks(); } while (! antrian .isEmpty () || processPromiseRejections()); setHasTickScheduled(salah); setHasRejectionToWarn(salah); }
Seperti yang dapat dilihat dari fungsi processTicksAndRejections(), fungsi callback dari antrian antrian pertama-tama dikeluarkan melalui loop while, dan fungsi callback dalam antrian antrian ditambahkan melalui process.nextTick. Saat perulangan while berakhir, fungsi runMicrotasks() dipanggil untuk menjalankan fungsi callback Promise.
struktur inti Node.js yang mengandalkan libuv dapat dibagi menjadi dua bagian. Satu bagian adalah I/O jaringan. Implementasi dasarnya akan bergantung pada API sistem yang berbeda menurut sistem operasi yang berbeda /O, DNS, dan kode pengguna. Bagian ini Gunakan kumpulan thread untuk pemrosesan.
Mekanisme inti libuv untuk menangani operasi asinkron adalah polling kejadian. Polling kejadian dibagi menjadi beberapa langkah. Operasi umumnya adalah melintasi dan menjalankan fungsi panggilan balik dalam antrian.
Terakhir, disebutkan bahwa API asinkron process.nextTick dan Promise tidak termasuk dalam polling acara. Penggunaan yang tidak tepat akan menyebabkan polling acara diblokir.