1. เหตุใด JavaScript จึงมีเธรดเดี่ยว?
คุณลักษณะที่สำคัญของภาษาจาวาสคริปต์คือเธรดเดี่ยวซึ่งหมายความว่าคุณสามารถทำสิ่งเดียวได้ในเวลาเดียวกัน เหตุใด JavaScript จึงมีหลายเธรดไม่ได้? สิ่งนี้จะปรับปรุงประสิทธิภาพ
เธรดเดี่ยวของ JavaScript เกี่ยวข้องกับวัตถุประสงค์ ในฐานะที่เป็นภาษาสคริปต์เบราว์เซอร์จุดประสงค์หลักของ JavaScript คือการโต้ตอบกับผู้ใช้และใช้งาน DOM สิ่งนี้กำหนดว่ามันสามารถเป็นเกลียวเดี่ยวได้มิฉะนั้นจะทำให้เกิดปัญหาการซิงโครไนซ์ที่ซับซ้อนมาก ตัวอย่างเช่นสมมติว่า JavaScript มีสองเธรดในเวลาเดียวกันเธรดหนึ่งจะเพิ่มเนื้อหาในโหนด DOM บางตัวและเธรดอื่น ๆ จะลบโหนดนี้ซึ่งเบราว์เซอร์ควรใช้เธรดใดในเวลานี้
ดังนั้นเพื่อหลีกเลี่ยงความซับซ้อน JavaScript เป็นเธรดเดียวตั้งแต่แรกเกิดซึ่งได้กลายเป็นคุณลักษณะหลักของภาษานี้และจะไม่เปลี่ยนแปลงในอนาคต
เพื่อใช้ประโยชน์จากพลังการคำนวณของ CPU หลายคอร์ HTML5 เสนอมาตรฐานผู้ปฏิบัติงานบนเว็บทำให้สคริปต์ JavaScript สร้างหลายเธรด แต่เธรดเด็กจะถูกควบคุมโดยเธรดหลักอย่างสมบูรณ์และไม่สามารถใช้งาน DOM ได้ ดังนั้นมาตรฐานใหม่นี้ไม่ได้เปลี่ยนลักษณะของการเธรดเดี่ยว JavaScript
2. คิวงาน
การทำเกลียวเดี่ยวหมายความว่างานทั้งหมดจะต้องเข้าคิวและงานก่อนหน้าจะถูกดำเนินการก่อนที่งานถัดไปจะถูกดำเนินการ หากงานก่อนหน้านี้ใช้เวลานานงานถัดไปจะต้องรอ
หากคิวเป็นเพราะการคำนวณจำนวนมากและ CPU ยุ่งเกินไปมันจะดี แต่หลายครั้งที่ CPU ไม่ได้ใช้งานเนื่องจากอุปกรณ์ IO (อุปกรณ์อินพุตและเอาต์พุต) ช้ามาก (เช่นการดำเนินการ AJAX อ่านข้อมูลจากเครือข่าย) และคุณต้องรอผลลัพธ์ที่จะออกมาก่อนที่จะดำเนินการ
ผู้ออกแบบภาษาจาวาสคริปต์ตระหนักว่าในเวลานี้ CPU สามารถเพิกเฉยต่ออุปกรณ์ IO ได้อย่างสมบูรณ์ระงับงานที่รอคอยและทำงานงานต่อไปก่อน รอจนกว่าอุปกรณ์ IO จะส่งคืนผลลัพธ์จากนั้นหันหลังกลับและดำเนินการต่องานที่ถูกระงับ
ดังนั้น JavaScript มีวิธีการดำเนินการสองวิธี: หนึ่งคือ CPU ดำเนินการตามลำดับงานก่อนหน้านี้จะสิ้นสุดลงและจากนั้นงานถัดไปจะถูกดำเนินการซึ่งเรียกว่าการดำเนินการแบบซิงโครนัส; อีกอย่างคือ CPU ข้ามงานด้วยเวลารอนานและประมวลผลงานที่ตามมาก่อนซึ่งเรียกว่าการดำเนินการแบบอะซิงโครนัส โปรแกรมเมอร์เลือกวิธีการดำเนินการแบบอิสระที่จะนำมาใช้
โดยเฉพาะกลไกการดำเนินงานของการดำเนินการแบบอะซิงโครนัสมีดังนี้ (เช่นเดียวกับการดำเนินการแบบซิงโครนัสเนื่องจากถือได้ว่าเป็นการดำเนินการแบบอะซิงโครนัสโดยไม่ต้องทำงานแบบอะซิงโครนัส)
(1) งานทั้งหมดจะดำเนินการบนเธรดหลักเพื่อสร้างสแต็กบริบทการดำเนินการ
(2) นอกเหนือจากเธรดหลักแล้วยังมี "คิวงาน" ระบบวางงานอะซิงโครนัสลงใน "คิวงาน" จากนั้นยังคงดำเนินงานต่อไป
(3) เมื่อมีการดำเนินการงานทั้งหมดใน "Execution Stack" ระบบจะอ่าน "คิวงาน" หากในเวลานี้งานอะซิงโครนัสได้สิ้นสุดสถานะการรอคอยมันจะเข้าสู่สแต็กการดำเนินการจาก "คิวงาน" และดำเนินการต่อ
(4) เธรดหลักยังคงทำซ้ำขั้นตอนที่สามด้านบน
รูปต่อไปนี้เป็นแผนผังแผนผังของเธรดหลักและคิวงาน
ตราบใดที่เธรดหลักว่างเปล่ามันจะอ่าน "คิวงาน" นี่คือกลไกการทำงานของ JavaScript กระบวนการนี้จะทำซ้ำอย่างต่อเนื่อง
3. กิจกรรมและฟังก์ชั่นการโทรกลับ
"คิวงาน" เป็นคิวของเหตุการณ์ (ยังเข้าใจว่าเป็นคิวข้อความ) เมื่ออุปกรณ์ IO ทำงานให้เสร็จสมบูรณ์มันจะเพิ่มเหตุการณ์ใน "คิวงาน" ซึ่งบ่งชี้ว่างานอะซิงโครนัสที่เกี่ยวข้องสามารถป้อน "สแต็กการดำเนินการ" เธรดหลักอ่าน "คิวงาน" ซึ่งหมายถึงการอ่านเหตุการณ์ที่อยู่ภายใน
เหตุการณ์ใน "คิวงาน" รวมถึงเหตุการณ์นอกเหนือจากเหตุการณ์จากอุปกรณ์ IO แต่ยังรวมถึงเหตุการณ์ที่ผู้ใช้สร้างขึ้น (เช่นการคลิกเมาส์การเลื่อนหน้า ฯลฯ ) ตราบใดที่มีการระบุฟังก์ชั่นการโทรกลับเหตุการณ์เหล่านี้จะเข้าสู่ "คิวงาน" เมื่อเกิดขึ้นและรอให้เธรดหลักอ่าน
สิ่งที่เรียกว่า "การโทรกลับ" คือรหัสที่จะถูกแขวนไว้โดยเธรดหลัก งานแบบอะซิงโครนัสต้องระบุฟังก์ชั่นการโทรกลับ เมื่องานอะซิงโครนัสกลับมาจาก "คิวงาน" ไปยังสแต็กการดำเนินการฟังก์ชันการโทรกลับจะถูกดำเนินการ
"คิวงาน" เป็นโครงสร้างข้อมูลครั้งแรกในครั้งแรกโดยมีเหตุการณ์ที่ได้รับการจัดอันดับก่อนและต้องการกลับไปที่เธรดหลัก กระบวนการอ่านของเธรดหลักนั้นเป็นไปโดยอัตโนมัติ ตราบใดที่สแต็กการดำเนินการถูกล้างเหตุการณ์แรกใน "คิวงาน" จะกลับไปที่เธรดหลักโดยอัตโนมัติ อย่างไรก็ตามเนื่องจากฟังก์ชั่น "ตัวจับเวลา" ที่กล่าวถึงในภายหลังเธรดหลักจำเป็นต้องตรวจสอบเวลาดำเนินการและเหตุการณ์บางอย่างจะต้องกลับไปที่เธรดหลักในเวลาที่กำหนด
4. Event Loop
เธรดหลักอ่านเหตุการณ์จาก "คิวงาน" กระบวนการนี้วนวนอย่างต่อเนื่องดังนั้นกลไกการทำงานทั้งหมดจึงเรียกว่า Event Loop
เพื่อให้เข้าใจถึงเหตุการณ์ที่ดีขึ้นโปรดดูภาพด้านล่าง (อ้างอิงจากคำพูดของฟิลิปโรเบิร์ตส์ "ช่วยฉันติดอยู่ในวงอีเวนต์")
ในรูปด้านบนเมื่อเธรดหลักกำลังทำงานอยู่มันจะสร้างกองและสแต็ก รหัสในสแต็กเรียก API ภายนอกต่าง ๆ ซึ่งเพิ่มเหตุการณ์ต่าง ๆ (คลิก, โหลด, เสร็จสิ้น) ไปยัง "คิวงาน" ตราบใดที่รหัสในสแต็กถูกดำเนินการเธรดหลักจะอ่าน "คิวงาน" และเรียกใช้ฟังก์ชันการโทรกลับที่สอดคล้องกับเหตุการณ์เหล่านั้นในทางกลับกัน
เรียกใช้รหัสในสแต็กดำเนินการเสมอก่อนที่จะอ่าน "คิวงาน" โปรดดูตัวอย่างต่อไปนี้
การคัดลอกรหัสมีดังนี้:
var req = ใหม่ xmlhttprequest ();
req.open ('get', url);
req.onload = function () {};
req.onerror = function () {};
req.send ();
วิธีการ REQ.Send ในรหัสด้านบนคือการดำเนินการ AJAX เพื่อส่งข้อมูลไปยังเซิร์ฟเวอร์ มันเป็นงานอะซิงโครนัสซึ่งหมายความว่าระบบจะอ่าน "คิวงาน" เฉพาะหลังจากรหัสทั้งหมดในสคริปต์ปัจจุบันถูกดำเนินการ ดังนั้นจึงเทียบเท่ากับวิธีการเขียนต่อไปนี้
การคัดลอกรหัสมีดังนี้:
var req = ใหม่ xmlhttprequest ();
req.open ('get', url);
req.send ();
req.onload = function () {};
req.onerror = function () {};
กล่าวคือส่วนของฟังก์ชันการโทรกลับที่ระบุ (ONLOAD และ ONERROR) ไม่สำคัญก่อนหรือหลังวิธีการส่ง () เพราะเป็นส่วนหนึ่งของสแต็กการดำเนินการและระบบจะดำเนินการเสมอก่อนที่จะอ่าน "คิวงาน"
5. ตัวจับเวลา
นอกเหนือจากการวางงานอะซิงโครนัสแล้ว "คิวงาน" ยังมีฟังก์ชั่นอื่นซึ่งก็คือการวางเหตุการณ์ที่กำหนดเวลานั่นคือระบุว่ารหัสบางอย่างจะถูกดำเนินการนานแค่ไหนหลังจากนั้น สิ่งนี้เรียกว่าฟังก์ชั่น "ตัวจับเวลา" ซึ่งเป็นรหัสที่ดำเนินการอย่างสม่ำเสมอ
ฟังก์ชั่นตัวจับเวลาส่วนใหญ่เสร็จสิ้นโดยสองฟังก์ชั่น: settimeout () และ setInterval () กลไกการทำงานภายในของพวกเขาเหมือนกันทุกประการ ความแตกต่างคือรหัสที่ระบุโดยอดีตจะถูกดำเนินการในครั้งเดียวในขณะที่หลังถูกดำเนินการซ้ำ ๆ ส่วนใหญ่จะกล่าวถึง settimeout ()
Settimeout () ยอมรับพารามิเตอร์สองตัวแรกคือฟังก์ชั่นการโทรกลับและที่สองคือจำนวนมิลลิวินาทีเพื่อเลื่อนการดำเนินการ
การคัดลอกรหัสมีดังนี้:
console.log (1);
settimeout (function () {console.log (2);}, 1000);
console.log (3);
ผลการดำเนินการของรหัสข้างต้นคือ 1, 3, 2 เนื่องจาก settimeout () จะทำให้บรรทัดที่สองล่าช้าจนกระทั่งหลังจาก 1,000 มิลลิวินาที
หากพารามิเตอร์ที่สองของ SetTimeOut () ถูกตั้งค่าเป็น 0 นั่นหมายความว่าฟังก์ชันการโทรกลับที่ระบุ (ช่วงเวลา 0 มิลลิวินาที) จะถูกดำเนินการทันทีหลังจากที่รหัสปัจจุบันถูกเรียกใช้งาน
การคัดลอกรหัสมีดังนี้:
settimeout (function () {console.log (1);}, 0);
console.log (2);
ผลการดำเนินการของรหัสข้างต้นอยู่เสมอ 2 และ 1 เนื่องจากระบบจะเรียกใช้ฟังก์ชันการโทรกลับใน "คิวงาน" หลังจากดำเนินการบรรทัดที่สองเท่านั้น
มาตรฐาน HTML5 ระบุว่าค่าต่ำสุด (ช่วงเวลาที่สั้นที่สุด) ของพารามิเตอร์ที่สองของ settimeout () ต้องไม่น้อยกว่า 4 มิลลิวินาที หากต่ำกว่าค่านี้มันจะเพิ่มขึ้นโดยอัตโนมัติ ก่อนหน้านี้เบราว์เซอร์ที่มีอายุมากกว่าจะกำหนดช่วงเวลาขั้นต่ำเป็น 10 มิลลิวินาที
นอกจากนี้สำหรับการเปลี่ยนแปลง DOM เหล่านั้น (โดยเฉพาะชิ้นส่วนที่เกี่ยวข้องกับการแสดงหน้าใหม่) พวกเขามักจะไม่ถูกดำเนินการทันที แต่ทุก ๆ 16 มิลลิวินาที ในเวลานี้ผลของการใช้ requestimationframe () ดีกว่า settimeout ()
ควรสังเกตว่า settimeout () เพียงแค่แทรกเหตุการณ์ลงใน "คิวงาน" คุณต้องรอจนกว่ารหัสปัจจุบัน (Execution Stack) จะถูกดำเนินการก่อนที่เธรดหลักจะเรียกใช้ฟังก์ชันการโทรกลับที่ระบุ หากรหัสปัจจุบันใช้เวลานานอาจใช้เวลานานในการรอดังนั้นจึงไม่มีการรับประกันว่าฟังก์ชันการโทรกลับจะถูกดำเนินการในเวลาที่ระบุโดย SetTimeOut ()
6. Node.js Event Loop
Node.js ยังเป็นลูปเหตุการณ์แบบเธรดเดี่ยว แต่กลไกการทำงานนั้นแตกต่างจากสภาพแวดล้อมของเบราว์เซอร์
โปรดดูแผนภาพด้านล่าง (ผู้แต่ง @busyrich)
ตามตัวเลขข้างต้นกลไกการทำงานของ node.js มีดังนี้
(1) V8 Engine Parses JavaScript Scripts
(2) รหัสที่แยกวิเคราะห์เรียกโหนด API
(3) ไลบรารี libuv รับผิดชอบการดำเนินการของโหนด API มันกำหนดงานที่แตกต่างกันให้กับเธรดที่แตกต่างกันสร้างลูปเหตุการณ์และส่งคืนผลลัพธ์การดำเนินการของงานไปยังเครื่องยนต์ V8 ในลักษณะอะซิงโครนัส
(4) เครื่องยนต์ V8 ส่งคืนผลลัพธ์ไปยังผู้ใช้
นอกเหนือจากสองวิธี settimeout และ setInterval แล้ว node.js ยังมีวิธีอื่นอีกสองวิธีที่เกี่ยวข้องกับ "คิวงาน": process.nexttick และ setimmediate พวกเขาสามารถช่วยให้เราเข้าใจ "คิวงาน" ได้ลึกซึ้งยิ่งขึ้น
วิธี process.nexttick สามารถเรียกใช้ฟังก์ชันการโทรกลับในตอนท้ายของ "การดำเนินการสแต็ค" ปัจจุบันก่อนที่เธรดหลักจะอ่าน "คิวงาน" ในครั้งต่อไป นั่นคืองานที่ระบุมักจะเกิดขึ้นก่อนงานแบบอะซิงโครนัสทั้งหมด เมธอด setimmediate ทริกเกอร์ฟังก์ชั่นการโทรกลับที่หางของ "คิวงาน" ปัจจุบันนั่นคืองานที่ระบุจะถูกดำเนินการในครั้งต่อไปที่เธรดหลักจะอ่าน "คิวงาน" ซึ่งคล้ายกับ settimout (fn, 0) โปรดดูตัวอย่างต่อไปนี้ (ผ่าน Stackoverflow)
การคัดลอกรหัสมีดังนี้:
process.nexttick (ฟังก์ชั่น a () {
console.log (1);
process.nexttick (ฟังก์ชั่น b () {console.log (2);});
-
settimeout (ฟังก์ชั่นหมดเวลา () {
console.log ('หมดเวลายิง');
}, 0)
// 1
// 2
// หมดเวลายิง
ในรหัสข้างต้นเนื่องจากฟังก์ชั่นการโทรกลับที่ระบุโดย process.nexttick วิธีการถูกทริกเกอร์ที่หางของ "สแต็กการดำเนินการ" ปัจจุบันไม่เพียง แต่ฟังก์ชั่น A จะถูกดำเนินการก่อนการหมดเวลาฟังก์ชั่นการโทรกลับที่ระบุโดย setTimeOut ซึ่งหมายความว่าหากมีหลายกระบวนการข้อความ NEXTTICK (ไม่ว่าพวกเขาจะซ้อนกันหรือไม่) พวกเขาทั้งหมดจะถูกดำเนินการใน "การดำเนินการสแต็ก" ปัจจุบัน
ทีนี้มาดู Setimmediate กันเถอะ
การคัดลอกรหัสมีดังนี้:
setimmediate (ฟังก์ชัน A () {
console.log (1);
setimmediate (ฟังก์ชัน b () {console.log (2);});
-
settimeout (ฟังก์ชั่นหมดเวลา () {
console.log ('หมดเวลายิง');
}, 0)
// 1
// หมดเวลายิง
// 2
ในรหัสข้างต้นมีสอง setimmediates SetImmediate แรกระบุว่าฟังก์ชันการโทรกลับ A ถูกเรียกใช้ที่หางของ "คิวงาน" ปัจจุบัน (The Next "Event Loop"); จากนั้น Settimeout ยังระบุว่าการหมดเวลาฟังก์ชั่นการโทรกลับถูกเรียกใช้ที่หางของ "คิวงาน" ในปัจจุบันดังนั้นในผลลัพธ์ผลลัพธ์การหมดเวลาการยิงจะถูกจัดอันดับหลัง 1. สำหรับการจัดอันดับ 2 หลังการยิงหมดเวลาเป็นเพราะคุณลักษณะที่สำคัญอีกประการหนึ่ง
เราได้รับความแตกต่างที่สำคัญจากสิ่งนี้: หลายกระบวนการคำสั่ง NEXTTICK จะถูกดำเนินการเสมอในครั้งเดียวในขณะที่ SetImMediates หลายรายการต้องดำเนินการหลายครั้ง อันที่จริงนี่เป็นเหตุผลว่าทำไม Node.js เวอร์ชัน 10.0 เพิ่มวิธี SetImmediate มิฉะนั้นการเรียกซ้ำไปยัง process.nexttick เช่นต่อไปนี้จะไม่มีที่สิ้นสุดและเธรดหลักจะไม่อ่าน "คิวเหตุการณ์" เลย!
การคัดลอกรหัสมีดังนี้:
process.nexttick (ฟังก์ชั่น foo () {
process.nexttick (foo);
-
ในความเป็นจริงตอนนี้ถ้าคุณเขียนกระบวนการเรียกซ้ำ nexttick, node.js จะส่งคำเตือนขอให้คุณเปลี่ยนเป็น setimmediate
นอกจากนี้เนื่องจากฟังก์ชั่นการโทรกลับที่ระบุโดย process.nexttick จะถูกกระตุ้นใน "ลูปเหตุการณ์" นี้และ setimmediate ระบุว่ามันถูกกระตุ้นใน "ลูปเหตุการณ์" ครั้งต่อไปจึงเห็นได้ชัดว่าอดีตเกิดขึ้นก่อนหน้านี้ก่อนหน้านี้และมีประสิทธิภาพมากขึ้นในการดำเนินการ (เพราะไม่จำเป็นต้องตรวจสอบ