JavaScript ภายใต้ประทุน
สารบัญ
- เธรดและคอลสแต็ก
- บริบทการดำเนินการ
- Event Loop และ Asynchronous JavaScript
- ที่เก็บความทรงจำและการเก็บขยะ
- JIT (ทันเวลาพอดี) กำลังรวบรวม
- สรุป
ในบทความนี้ เราจะเจาะลึกถึงการทำงานภายในของ JavaScript และวิธีการทำงานจริง เมื่อเข้าใจรายละเอียด คุณจะเข้าใจลักษณะการทำงานของโค้ดของคุณ ดังนั้นคุณจึงสามารถเขียนแอปได้ดีขึ้น
JavaScript ถูกอธิบายว่าเป็น:
เธรดเดี่ยว รวบรวมขยะ ตีความ หรือภาษาโปรแกรมคอมไพล์ Just In Time พร้อมลูปเหตุการณ์ที่ไม่ปิดกั้น
เรามาแกะคำศัพท์สำคัญเหล่านี้กัน
เธรด & กองโทร:
เอ็นจิ้น JavaScript เป็นล่ามแบบเธรดเดียวที่ประกอบด้วยฮีปและ คอลสแต็ก เดียวที่ใช้ในการรันโปรแกรม Call Stack
เป็น โครงสร้างข้อมูลที่ใช้หลักการ Last In, First Out (LIFO) เพื่อจัดเก็บและจัดการการเรียกใช้ฟังก์ชัน (การโทร) เป็นการชั่วคราว หมายความว่าฟังก์ชันสุดท้ายที่ถูกพุชเข้าไปในสแต็กจะเป็นฟังก์ชันแรกที่เด้งออกมาเมื่อฟังก์ชันส่งกลับ เนื่องจาก call stack เป็นแบบเดี่ยว การดำเนินการของฟังก์ชันจะทำทีละรายการจากบนลงล่าง หมายความว่าคอลสแต็กเป็นแบบซิงโครนัส
ตอนนี้เนื่องจากเป็นซิงโครนัส คุณจะสงสัยว่า JavaScript จะจัดการกับการโทรแบบอะซิงโครนัสได้อย่างไร
เหตุการณ์วนซ้ำเป็นความลับเบื้องหลังการเขียนโปรแกรมอะซิงโครนัสของ JavaScript
แต่ก่อนที่จะอธิบายแนวคิดของการเรียกใช้ async ภายใน JavaScript และความเป็นไปได้อย่างไรกับภาษาแบบเธรดเดียว เรามาทำความเข้าใจกันก่อนว่าโค้ดถูกเรียกใช้งานอย่างไร
บริบทการดำเนินการ (EC):
บริบทการดำเนินการถูกกำหนดให้เป็นสภาพแวดล้อมที่โค้ด JavaScript ถูกดำเนินการ
การสร้าง Execution Context เกิดขึ้นในสองขั้นตอน:
1. ขั้นตอนการสร้างความทรงจำ:
- การสร้างวัตถุส่วนกลาง (ซึ่งเรียกว่าวัตถุหน้าต่างในเบราว์เซอร์และวัตถุส่วนกลางใน NodeJS)
- สร้างวัตถุ "นี้" และเชื่อมโยงกับวัตถุส่วนกลาง
- การตั้งค่าฮีปหน่วยความจำ (ฮีปเป็นขอบเขตของหน่วยความจำขนาดใหญ่ ส่วนใหญ่ไม่มีโครงสร้าง) สำหรับจัดเก็บตัวแปรและการอ้างอิงฟังก์ชัน
- การจัดเก็บฟังก์ชันและตัวแปรในบริบทการดำเนินการทั่วโลกโดยใช้Hoisting
ตอนนี้เรารู้ขั้นตอนเบื้องหลังการทำงานของโค้ดแล้ว เรามากลับไปที่
วนรอบเหตุการณ์:
ขั้นแรก เรามาเริ่มด้วยการดูแผนภาพนี้:
เรามีกลไกที่ประกอบด้วยสององค์ประกอบหลัก:
* Memory Heap — นี่คือที่ที่การจัดสรรหน่วยความจำเกิดขึ้น
* Call Stack — นี่คือที่ที่เฟรมสแต็กของคุณอยู่ขณะที่โค้ดของคุณทำงาน
เรามี Web API ซึ่งเป็นเธรดที่คุณไม่สามารถเข้าถึงได้ คุณสามารถเรียกใช้ได้ พวกเขาเป็นส่วนหนึ่งของเบราว์เซอร์ที่เริ่มทำงานพร้อมกัน เช่น DOM, AJAX, setTimeout และอื่นๆ อีกมากมาย
สุดท้ายคือคิวการโทรกลับซึ่งเป็นรายการเหตุการณ์ที่ต้องดำเนินการ แต่ละเหตุการณ์มีฟังก์ชันที่เกี่ยวข้องซึ่งถูกเรียกให้จัดการ
แล้วงานของ event loop ที่นี่คืออะไร?
Event Loop มีหน้าที่ง่าย ๆ อย่างหนึ่ง — ในการตรวจสอบ Call Stack และ Callback Queue หาก Call Stack ว่างเปล่า Event Loop จะรับเหตุการณ์แรกจากคิวและจะส่งไปยัง Call Stack ซึ่งจะเรียกใช้งานอย่างมีประสิทธิภาพ
การวนซ้ำดังกล่าวเรียกว่าการติ๊กใน Event Loop แต่ละเหตุการณ์เป็นเพียงการเรียกกลับของฟังก์ชัน
ที่เก็บความทรงจำและการเก็บขยะ:
เพื่อให้เข้าใจถึงความจำเป็นในการเก็บขยะ ก่อนอื่นเราต้องเข้าใจวงจรชีวิตของหน่วยความจำซึ่งค่อนข้างจะเหมือนกันสำหรับภาษาการเขียนโปรแกรมใดๆ มันมี 3 ขั้นตอนหลัก
1. จัดสรรหน่วยความจำ
2. ใช้หน่วยความจำที่จัดสรรเพื่ออ่านหรือเขียนหรือทั้งสองอย่าง
3. ปล่อยหน่วยความจำที่จัดสรรไว้เมื่อไม่ต้องการอีกต่อไป
ปัญหาการจัดการหน่วยความจำส่วนใหญ่เกิดขึ้นเมื่อเราพยายามปล่อยหน่วยความจำที่จัดสรร ข้อกังวลหลักที่เกิดขึ้นคือการกำหนดทรัพยากรหน่วยความจำที่ไม่ได้ใช้
ในกรณีของภาษาระดับต่ำที่นักพัฒนาต้องตัดสินใจด้วยตนเองเมื่อไม่ต้องการใช้หน่วยความจำอีกต่อไป ภาษาระดับสูง เช่น JavaScript จะใช้รูปแบบการจัดการหน่วยความจำอัตโนมัติที่เรียกว่า Garbage Collection(GC)
JavaScript ใช้สองกลยุทธ์ที่มีชื่อเสียงในการดำเนินการ GC: เทคนิคการนับอ้างอิงและอัลกอริทึม Mark-and-sweep
นี่คือคำอธิบายโดยละเอียดจากMDNเกี่ยวกับทั้งอัลกอริทึมและวิธีการทำงาน
JIT (ทันเวลาพอดี) รวบรวม:
กลับไปที่คำจำกัดความของ JavaScript: มันเขียนว่า “แปลภาษาโปรแกรมที่คอมไพล์โดย JIT” แล้วนั่นหมายความว่าอย่างไร เริ่มต้นด้วยความแตกต่างระหว่างคอมไพเลอร์และล่ามโดยทั่วไปอย่างไร
ลองนึกถึงคนสองคนที่มีภาษาต่างกันซึ่งต้องการสื่อสารกัน การเรียบเรียงเปรียบเสมือนการหยุดและใช้เวลาทั้งหมดเพื่อเรียนรู้ภาษา และการตีความจะเหมือนกับมีคนคอยตีความแต่ละประโยค
ดังนั้นภาษาที่คอมไพล์จึงมีเวลาเขียนที่ช้าและรันไทม์ที่เร็ว และภาษาที่ตีความได้จะตรงกันข้าม
พูดเป็นศัพท์เทคนิค: การคอมไพล์เป็นกระบวนการของการครอบคลุมซอร์สโค้ดของโปรแกรมเป็นไบนารีโค้ดที่เครื่องอ่านได้ ก่อนดำเนินการ และคอมไพเลอร์จะรวมโปรแกรมทั้งหมดไว้ในคราวเดียว
ในทางกลับกัน ล่ามคือโปรแกรมที่รันคำสั่งโปรแกรมโดยไม่ต้องคอมไพล์ล่วงหน้าให้อยู่ในรูปแบบที่เครื่องอ่านได้ และต้องใช้โค้ดบรรทัดเดียวต่อครั้ง
และนี่คือบทบาทการรวบรวม JIT ซึ่งกำลังปรับปรุงประสิทธิภาพของโปรแกรมตีความ รหัสทั้งหมดจะถูกแปลงเป็นรหัสเครื่องในครั้งเดียวและดำเนินการทันที
ภายในคอมไพเลอร์ JIT เรามีส่วนประกอบใหม่ที่เรียกว่าจอภาพ จอภาพนั้นเฝ้าดูรหัสขณะที่มันทำงานและ
- ระบุส่วนประกอบที่ร้อนหรืออุ่นของรหัส เช่น รหัสซ้ำ
- แปลงจากส่วนประกอบเหล่านั้นเป็นรหัสเครื่องในระหว่างรันไทม์
- เพิ่มประสิทธิภาพรหัสเครื่องที่สร้างขึ้น
- Hot swap การดำเนินการก่อนหน้าของรหัส
ตอนนี้เราเข้าใจแนวคิดหลักแล้ว มาใช้เวลาสักครู่เพื่อรวบรวมทุกอย่างเข้าด้วยกันและสรุปขั้นตอนที่JS Engineปฏิบัติตามในขณะที่ดำเนินการโค้ด:
- JS Engine นำโค้ด JS ที่เขียนด้วยไวยากรณ์ที่มนุษย์อ่านได้ และเปลี่ยนเป็นโค้ดเครื่อง
- เครื่องยนต์ใช้ parser เพื่ออ่านโค้ดทีละบรรทัดและตรวจสอบว่าไวยากรณ์ถูกต้องหรือไม่ หากมีข้อผิดพลาดใดๆ โค้ดจะหยุดดำเนินการและข้อผิดพลาดจะถูกส่งออกไป
- หากผ่านการตรวจสอบทั้งหมด ตัวแยกวิเคราะห์จะสร้างโครงสร้างข้อมูลแบบต้นไม้ที่เรียกว่า Abstract Syntax Tree (AST)
- AST เป็นโครงสร้างข้อมูลที่แสดงถึงรหัสในโครงสร้างแบบต้นไม้ การเปลี่ยนรหัสเป็นรหัสเครื่องจาก AST นั้นง่ายกว่า
- จากนั้นล่ามจะดำเนินการรับ AST และเปลี่ยนเป็น IR ซึ่งเป็นนามธรรมของรหัสเครื่องและเป็นตัวกลางระหว่างรหัส JS และรหัสเครื่อง IR ยังอนุญาตให้ทำการปรับให้เหมาะสมและเคลื่อนที่ได้มากขึ้น
- จากนั้นคอมไพเลอร์ JIT จะใช้ IR ที่สร้างขึ้นและเปลี่ยนเป็นรหัสเครื่อง โดยการคอมไพล์โค้ด รับคำติชมในทันที และใช้คำติชมนั้นเพื่อปรับปรุงกระบวนการคอมไพล์
ขอบคุณสำหรับการอ่าน :)
คุณสามารถติดตามฉันได้ที่TwitterและLinkedIn