วิธีเริ่มต้นใช้งาน 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 แทน