
วิธีเริ่มต้นใช้งาน VUE3.0 อย่างรวดเร็ว: เข้าสู่การเรียนรู้
เมื่อพูดถึง Node.js ฉันเชื่อว่าวิศวกรส่วนหน้าส่วนใหญ่จะคิดถึงการพัฒนาเซิร์ฟเวอร์โดยยึดตามมัน คุณเพียงแค่ต้องเชี่ยวชาญ JavaScript เท่านั้นจึงจะกลายมาเป็น Full-Stack วิศวกร แต่จริงๆ แล้วความหมายของ Node.js และนั่นไม่ใช่ทั้งหมด
สำหรับภาษาระดับสูงหลายภาษา สิทธิ์ในการดำเนินการสามารถเข้าถึงระบบปฏิบัติการได้ แต่ JavaScript ที่ทำงานบนฝั่งเบราว์เซอร์นั้นเป็นข้อยกเว้น สภาพ แวดล้อมแซนด์บ็อกซ์ที่สร้างขึ้นโดยเบราว์เซอร์ จะผนึกวิศวกรส่วนหน้าในหอคอยงาช้างในโลกการเขียนโปรแกรม อย่างไรก็ตาม การเกิดขึ้นของ Node.js ได้ชดเชยข้อบกพร่องนี้ และวิศวกรส่วนหน้าก็สามารถไปถึงจุดต่ำสุดของโลกคอมพิวเตอร์ได้เช่นกัน
ดังนั้นความสำคัญของ Nodejs ต่อวิศวกรส่วนหน้าไม่เพียงแต่ให้ความสามารถในการพัฒนาแบบฟูลสแตกเท่านั้น แต่ที่สำคัญกว่านั้นคือการเปิดประตูสู่โลกพื้นฐานของคอมพิวเตอร์สำหรับวิศวกรส่วนหน้า บทความนี้เปิดประตูนี้โดยการวิเคราะห์หลักการใช้งาน Node.js
มีการขึ้นต่อกันมากกว่าหนึ่งโหลในไดเร็กทอรี /deps ของคลังซอร์สโค้ด Node.js รวมถึงโมดูลที่เขียนด้วยภาษา C (เช่น libuv, V8) และโมดูลที่เขียนด้วยภาษา JavaScript (เช่น Acorn , ปลั๊กอินโอ๊ก) ดังที่แสดงด้านล่าง

สิ่งที่สำคัญที่สุดคือโมดูลที่สอดคล้องกับไดเร็กทอรี v8 และ uv V8 เองไม่มีความสามารถในการทำงานแบบอะซิงโครนัส แต่ถูกนำไปใช้งานด้วยความช่วยเหลือของเธรดอื่นในเบราว์เซอร์ นี่คือสาเหตุที่เรามักพูดว่า js เป็นแบบเธรดเดียว เนื่องจากกลไกการแยกวิเคราะห์รองรับเฉพาะโค้ดการแยกวิเคราะห์แบบซิงโครนัสเท่านั้น แต่ใน Node.js การใช้งานแบบอะซิงโครนัสนั้นอาศัย libuv เป็นหลัก
libuv เป็นไลบรารี I/O แบบอะซิงโครนัสที่เขียนด้วยภาษา C ที่รองรับหลายแพลตฟอร์ม โดยส่วนใหญ่จะแก้ปัญหาการทำงานของ I/O ที่ทำให้เกิดการบล็อกได้ง่าย เดิมได้รับการพัฒนาเพื่อใช้กับ Node.js โดยเฉพาะ แต่ตั้งแต่นั้นมาก็มีการใช้งานโดยโมดูลอื่นๆ เช่น Luvit, Julia และ pyuv รูปด้านล่างเป็นแผนภาพโครงสร้างของ libuv

libuv มีวิธีการใช้งานแบบอะซิงโครนัสสองวิธี ซึ่งเป็นสองส่วนที่เลือกโดยกล่องสีเหลืองทางซ้ายและขวาของภาพด้านบน
ส่วนด้านซ้ายคือโมดูล I/O เครือข่าย ซึ่งมีกลไกการใช้งานที่แตกต่างกันภายใต้แพลตฟอร์มที่แตกต่างกัน ระบบ Linux ใช้ epoll เพื่อใช้งาน OSX และระบบ BSD อื่นๆ ใช้ KQueue ระบบ SunOS ใช้พอร์ตเหตุการณ์ และระบบ Windows ใช้ IOCP เนื่องจากเกี่ยวข้องกับ API พื้นฐานของระบบปฏิบัติการ จึงเข้าใจยากกว่า ดังนั้นฉันจะไม่แนะนำที่นี่
ส่วนด้านขวาประกอบด้วยโมดูลไฟล์ I/O, โมดูล DNS และโค้ดผู้ใช้ ซึ่งใช้การดำเนินการแบบอะซิงโครนัสผ่านเธรดพูล ไฟล์ I/O แตกต่างจากเครือข่าย I/O libuv ไม่ได้ขึ้นอยู่กับ API พื้นฐานของระบบ แต่ดำเนินการกับไฟล์ I/O ที่ถูกบล็อกในเธรดพูลส่วนกลาง
รูปต่อไปนี้คือแผนภาพเวิร์กโฟลว์การหยั่งเสียงเหตุการณ์ที่กำหนดโดยเว็บไซต์อย่างเป็นทางการของ libuv มาวิเคราะห์พร้อมกับโค้ดกัน

โค้ดหลักของลูปเหตุการณ์ libuv ถูกนำมาใช้ในฟังก์ชัน uv_run() ต่อไปนี้เป็นส่วนหนึ่งของโค้ดหลักภายใต้ระบบ Unix แม้ว่าจะเขียนด้วยภาษา C แต่ก็เป็นภาษาระดับสูงเช่น JavaScript ดังนั้นจึงเข้าใจได้ไม่ยากเกินไป ความแตกต่างที่ใหญ่ที่สุดอาจเป็นเครื่องหมายดอกจันและลูกศร เราสามารถเพิกเฉยต่อเครื่องหมายดอกจันได้ ตัวอย่างเช่น uv_loop_t* loop ในพารามิเตอร์ฟังก์ชันสามารถเข้าใจได้ว่าเป็น loop แบบแปรผันประเภท uv_loop_t ลูกศร "→" สามารถเข้าใจได้ว่าเป็นจุด "." ตัวอย่างเช่น loop→stop_flag สามารถเข้าใจได้ใน loop.stop_flag
int uv_run (uv_loop_t* วนซ้ำ, โหมด uv_run_mode) {
-
r = uv__loop_alive(ลูป);
ถ้า (!r) uv__update_time(วนซ้ำ);
ในขณะที่ (r != 0 && วนซ้ำ - >stop_flag == 0) {
uv__update_time(วนซ้ำ);
uv__run_timers(วนซ้ำ);
ran_pending = uv__run_pending(วนซ้ำ);
uv__run_idle(วนซ้ำ);
uv__run_prepare (วนซ้ำ);...uv__io_poll (วนซ้ำ หมดเวลา);
uv__run_check(วนซ้ำ);
uv__run_closing_handles (วนซ้ำ);...
-
} uv__loop_alive
ฟังก์ชันนี้ใช้เพื่อกำหนดว่าการสำรวจเหตุการณ์ควรดำเนินต่อไปหรือไม่ หากไม่มีงานที่ใช้งานอยู่ในออบเจ็กต์ลูป ฟังก์ชันนี้จะส่งคืน 0 และออกจากลูป
ในภาษา C "งาน" นี้มีชื่อทางวิชาชีพคือ "handle" ซึ่งสามารถเข้าใจได้ว่าเป็นตัวแปรที่ชี้ไปที่งาน หมายเลขอ้างอิงสามารถแบ่งออกเป็นสองประเภท: คำขอและหมายเลขอ้างอิง ซึ่งแสดงถึงหมายเลขอ้างอิงวงจรชีวิตสั้นและหมายเลขอ้างอิงวงจรอายุการใช้งานยาวนานตามลำดับ รหัสเฉพาะมีดังนี้:
static int uv__loop_alive(const uv_loop_t * loop) {
กลับ uv__has_active_handles (วนซ้ำ) ||. uv__has_active_reqs (วนซ้ำ) ||. วนซ้ำ - >closing_handles != NULL;
} uv__update_time
เพื่อลดจำนวนการเรียกของระบบที่เกี่ยวข้องกับเวลา ฟังก์ชันนี้ใช้เพื่อแคชเวลาของระบบปัจจุบัน ความแม่นยำสูงมากและสามารถเข้าถึงระดับนาโนวินาที แต่หน่วยยังคงเป็นมิลลิวินาที
ซอร์สโค้ดเฉพาะมีดังนี้:
UV_UNUSED(static void uv__update_time(uv_loop_t * loop)) {
วนซ้ำ - >เวลา = uv__hrtime(UV_CLOCK_FAST) / 1000000;
} uv__run_timers
เรียกใช้ฟังก์ชันการโทรกลับที่ถึงเกณฑ์เวลาใน setTimeout() และ setInterval() กระบวนการดำเนินการนี้ถูกนำมาใช้สำหรับการสำรวจเส้นทางแบบวนซ้ำ ดังที่คุณเห็นจากโค้ดด้านล่าง การเรียกกลับของตัวจับเวลาจะถูกจัดเก็บไว้ในข้อมูลของโครงสร้างฮีปขั้นต่ำ ซึ่งจะออกเมื่อฮีปขั้นต่ำว่างเปล่าหรือไม่ถึงขีดจำกัดเวลา .
ลบตัวจับเวลาออกก่อนดำเนินการฟังก์ชันโทรกลับตัวจับเวลา หากตั้งค่าการทำซ้ำ จะต้องเพิ่มลงในฮีปขั้นต่ำอีกครั้ง จากนั้นจึงดำเนินการเรียกกลับตัวจับเวลา
รหัสเฉพาะมีดังนี้:
void uv__run_timers(uv_loop_t * loop) {
struct heap_node * heap_node;
uv_timer_t * จัดการ;
สำหรับ (;;) {
heap_node = heap_min(timer_heap(วนซ้ำ));
ถ้า (heap_node == NULL) พัง;
หมายเลขอ้างอิง = container_of (heap_node, uv_timer_t, heap_node);
ถ้า (จัดการ - >หมดเวลา > วนซ้ำ - >เวลา) พัง;
uv_timer_stop(ที่จับ);
uv_timer_อีกครั้ง(จัดการ);
จัดการ ->timer_cb (จัดการ);
-
} uv__run_pending
สำรวจฟังก์ชันการเรียกกลับ I/O ทั้งหมดที่จัดเก็บไว้ใน pending_queue และส่งคืน 0 เมื่อ pending_queue ว่างเปล่า มิฉะนั้น จะส่งคืน 1 หลังจากเรียกใช้ฟังก์ชันการเรียกกลับใน pending_queue
รหัสดังต่อไปนี้:
static int uv__run_pending(uv_loop_t * loop) {
คิว * คิว;
คิว pq;
uv__io_t * w;
ถ้า (QUEUE_EMPTY( & loop - >pending_queue)) ส่งกลับ 0;
QUEUE_MOVE( & วนซ้ำ - >pending_queue, &pq);
ในขณะที่ (!QUEUE_EMPTY( & pq)) {
q = QUEUE_HEAD( & pq);
QUEUE_REMOVE(คิว);
QUEUE_INIT(q);
w = QUEUE_DATA(q, uv__io_t, รอดำเนินการ_queue);
w ->cb(ลูป, w, POLLOUT);
-
กลับ 1;
} ฟังก์ชันทั้งสาม uvrun_idle / uvrun_prepare / uv__run_check
ทั้งหมดถูกกำหนดผ่านฟังก์ชันมาโคร UV_LOOP_WATCHER_DEFINE ฟังก์ชันมาโครสามารถเข้าใจได้ว่าเป็นเทมเพลตโค้ดหรือฟังก์ชันที่ใช้ในการกำหนดฟังก์ชัน ฟังก์ชันมาโครถูกเรียกสามครั้งและส่งค่าพารามิเตอร์ชื่อเตรียม ตรวจสอบ และไม่ได้ใช้งานตามลำดับ ในเวลาเดียวกัน มีการกำหนดฟังก์ชันสามรายการ ได้แก่ uvrun_idle, uvrun_prepare และ uv__run_check
ดังนั้นตรรกะการดำเนินการจึงสอดคล้องกัน ทั้งหมดวนซ้ำและนำอ็อบเจ็กต์ในคิวคิว->ชื่อ##_จัดการตามหลักการเข้าก่อนออกก่อน จากนั้นจึงดำเนินการฟังก์ชันเรียกกลับที่สอดคล้องกัน
#define UV_LOOP_WATCHER_DEFINE(ชื่อ, ประเภท)
เป็นโมฆะ uv__run_## ชื่อ (uv_loop_t* วนซ้ำ) {
uv_##ชื่อ##_t* h;
คิวคิว;
คิว* คิว;
QUEUE_MOVE(&loop->ชื่อ##_handles, &คิว);
ในขณะที่ (!QUEUE_EMPTY(&คิว)) {
q = QUEUE_HEAD(&คิว);
h = QUEUE_DATA(q, uv_##name##_t, คิว);
QUEUE_REMOVE(คิว);
QUEUE_INSERT_TAIL(&วนซ้ำ->ชื่อ##_handles, q);
h->ชื่อ##_cb(h);
-
-
UV_LOOP_WATCHER_DEFINE(เตรียมตัว เตรียมใจ)
UV_LOOP_WATCHER_DEFINE (ตรวจสอบ ตรวจสอบ)
UV_LOOP_WATCHER_DEFINE(idle, IDLE) uv__io_poll
uv__io_poll ส่วนใหญ่จะใช้ในการสำรวจการดำเนินการ I/O การใช้งานเฉพาะจะแตกต่างกันไปขึ้นอยู่กับระบบปฏิบัติการ เราใช้ระบบ Linux เป็นตัวอย่างในการวิเคราะห์
ฟังก์ชัน uv__io_poll มีซอร์สโค้ดจำนวนมาก แกนหลักคือโค้ดลูปสองส่วน ส่วนหนึ่งของโค้ดมีดังนี้:
void uv__io_poll(uv_loop_t * loop, int timeout) {
ในขณะที่ (!QUEUE_EMPTY( & วนซ้ำ - >watcher_queue)) {
q = QUEUE_HEAD( & วนซ้ำ ->watcher_queue);
QUEUE_REMOVE(คิว);
QUEUE_INIT(q);
w = QUEUE_DATA(q, uv__io_t, watcher_queue);
e.events = w ->pevents;
e.data.fd = w ->fd;
ถ้า (w - >เหตุการณ์ == 0) op = EPOLL_CTL_ADD;
อย่างอื่นสหกรณ์ = EPOLL_CTL_MOD;
ถ้า (epoll_ctl(วนซ้ำ - >backend_fd, op, w - >fd, &e)) {
ถ้า (errno != EEXIST) ยกเลิก ();
ถ้า (epoll_ctl(loop - >backend_fd, EPOLL_CTL_MOD, w - >fd, &e)) ยกเลิก();
-
w -> เหตุการณ์ = w -> กิจกรรม;
-
สำหรับ (;;) {
สำหรับ (i = 0; i < nfds; i++) {
pe = เหตุการณ์ + i;
fd = pe ->data.fd;
w = วนซ้ำ -> ผู้เฝ้าดู [fd];
pe - >เหตุการณ์ &= w - >pevents |. POLLHUP;
ถ้า (pe - >เหตุการณ์ == POLLERR || pe - >เหตุการณ์ == POLLHUP) pe - >เหตุการณ์ |= w - >pevents & (POLLIN | POLLOUT | UV__POLLRDHUP | UV__POLLPRI);
ถ้า (pe - >เหตุการณ์ != 0) {
ถ้า (w == &loop - >signal_io_watcher) have_signals = 1;
อย่างอื่น w ->cb(loop, w, pe -> events);
เนเวนส์++;
-
-
ถ้า (have_signals != 0) วนซ้ำ ->signal_io_watcher.cb(วนซ้ำ &วนรอบ - >signal_io_watcher, POLLIN);
-
} ใน while loop ให้สำรวจคิวผู้สังเกตการณ์ watcher_queue นำเหตุการณ์และตัวอธิบายไฟล์ออกมาแล้วกำหนดให้กับอ็อบเจ็กต์เหตุการณ์ e จากนั้นเรียกใช้ฟังก์ชัน epoll_ctl เพื่อลงทะเบียนหรือแก้ไขเหตุการณ์ epoll
ใน for loop ตัวอธิบายไฟล์ที่รออยู่ใน epoll จะถูกนำออกและกำหนดให้กับ nfds จากนั้น nfds จะถูกสำรวจเพื่อดำเนินการฟังก์ชันเรียกกลับ
uv__run_closing_handles
ข้ามคิวที่รอการปิด ปิดตัวจัดการ เช่น stream, tcp, udp ฯลฯ จากนั้นเรียก close_cb ที่สอดคล้องกับตัวจัดการ รหัสดังต่อไปนี้:
โมฆะคงที่ uv__run_closing_handles(uv_loop_t * loop) {
uv_handle_t * p;
uv_handle_t * คิว;
p = วนรอบ -> closes_handles;
วนซ้ำ ->closing_handles = NULL;
ในขณะที่ (p) {
q = p ->next_closing;
uv__finish_close(พี);
พี = คิว;
-
} แม้ว่า process.nextTick และ Promise จะเป็นทั้ง API แบบอะซิงโครนัส แต่ก็ไม่ได้เป็นส่วนหนึ่งของการสำรวจเหตุการณ์ ทั้งสองมีคิวงานของตนเอง ซึ่งจะดำเนินการหลังจากแต่ละขั้นตอนของการสำรวจเหตุการณ์เสร็จสิ้น ดังนั้น เมื่อเราใช้ API แบบอะซิงโครนัสทั้งสองนี้ เราต้องให้ความสนใจ หากมีการทำงานที่ยาวนานหรือการเรียกซ้ำในฟังก์ชันการโทรกลับที่เข้ามา การโพลเหตุการณ์จะถูกบล็อก ซึ่งจะทำให้การดำเนินการ I/O "หิวโหย"
รหัสต่อไปนี้เป็นตัวอย่างที่ฟังก์ชันการเรียกกลับของ fs.readFile ไม่สามารถดำเนินการได้โดยการเรียก prcoess.nextTick แบบวนซ้ำ
fs.readFile('config.json', (ผิดพลาด, ข้อมูล) = >{...
}) const ทราเวิร์ส = () = >{
กระบวนการ nextTick (สำรวจ)
} เพื่อแก้ไขปัญหานี้ คุณสามารถใช้ setImmediate แทนได้ เนื่องจาก setImmediate จะดำเนินการคิวฟังก์ชันการเรียกกลับในลูปเหตุการณ์ คิวงาน process.nextTick มีลำดับความสำคัญสูงกว่าคิวงาน Promise ด้วยเหตุผลเฉพาะ โปรดดูโค้ดต่อไปนี้:
function processTicksAndRejections() {
ปล่อยให้ต็อก;
ทำ {
ในขณะที่ (tock = Queue.shift()) {
const asyncId = tock[async_id_สัญลักษณ์];
emitBefore (asyncId, tock [trigger_async_id_สัญลักษณ์], tock);
พยายาม {
const โทรกลับ = tock.callback;
ถ้า (tock.args === ไม่ได้กำหนด) {
โทรกลับ ();
} อื่น {
const args = tock.args;
สวิตช์ (ความยาว args) {
กรณีที่ 1:
โทรกลับ (args [0]);
หยุดพัก;
กรณีที่ 2:
โทรกลับ (args [0], args [1]);
หยุดพัก;
กรณีที่ 3:
โทรกลับ (args [0], args [1], args [2]);
หยุดพัก;
กรณีที่ 4:
โทรกลับ (args [0], args [1], args [2], args [3]);
หยุดพัก;
ค่าเริ่มต้น:
โทรกลับ(...args);
-
-
} ในที่สุด {
ถ้า (destroyHooksExist()) ปล่อย Destroy(asyncId);
-
emitAfter(asyncId);
-
รันไมโครทาสก์();
} ในขณะที่ (! คิว . isEmpty () || processPromiseRejections());
setHasTickScheduled (เท็จ);
setHasRejectionToWarn (เท็จ);
} ดังที่เห็นได้จากฟังก์ชัน processTicksAndRejections() ฟังก์ชันการเรียกกลับของคิวคิวจะถูกนำออกก่อนผ่าน while loop และฟังก์ชันการเรียกกลับในคิวคิวจะถูกเพิ่มผ่าน process.nextTick เมื่อลูป while สิ้นสุดลง ฟังก์ชัน runMicrotasks() จะถูกเรียกเพื่อดำเนินการฟังก์ชัน Promise callback
โครงสร้างหลักของ Node.js ที่ใช้ libuv สามารถแบ่งออกเป็นสองส่วน ส่วนหนึ่งคือ I/O เครือข่าย การใช้งานพื้นฐานจะขึ้นอยู่กับ API ของระบบที่แตกต่างกันตามระบบปฏิบัติการที่แตกต่างกัน /O, DNS และรหัสผู้ใช้ ส่วนนี้ใช้เธรดพูลสำหรับการประมวลผล
กลไกหลักของ libuv ในการจัดการการดำเนินการแบบอะซิงโครนัสคือการโพลเหตุการณ์แบ่งออกเป็นหลายขั้นตอน การดำเนินการทั่วไปคือการสำรวจและดำเนินการฟังก์ชันโทรกลับในคิว
ท้ายที่สุด มีการกล่าวถึงว่ากระบวนการ API แบบอะซิงโครนัส nextTick และ Promise ไม่ได้อยู่ในการสำรวจเหตุการณ์ การใช้งานที่ไม่เหมาะสมจะทำให้การสำรวจเหตุการณ์ถูกบล็อก วิธีแก้ปัญหาหนึ่งคือใช้ setImmediate แทน