JavaScript ภายใต้ประทุน

Nov 28 2022
สารบัญ ในบทความนี้ เราจะเจาะลึกการทำงานภายในของ 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

ตอนนี้เรารู้ขั้นตอนเบื้องหลังการทำงานของโค้ดแล้ว เรามากลับไปที่

วนรอบเหตุการณ์:

ขั้นแรก เรามาเริ่มด้วยการดูแผนภาพนี้:

ลูปเหตุการณ์ใน JS

เรามีกลไกที่ประกอบด้วยสององค์ประกอบหลัก:
* 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ปฏิบัติตามในขณะที่ดำเนินการโค้ด:

แหล่งที่มาของภาพ: ช่อง Traversy Media
  1. JS Engine นำโค้ด JS ที่เขียนด้วยไวยากรณ์ที่มนุษย์อ่านได้ และเปลี่ยนเป็นโค้ดเครื่อง
  2. เครื่องยนต์ใช้ parser เพื่ออ่านโค้ดทีละบรรทัดและตรวจสอบว่าไวยากรณ์ถูกต้องหรือไม่ หากมีข้อผิดพลาดใดๆ โค้ดจะหยุดดำเนินการและข้อผิดพลาดจะถูกส่งออกไป
  3. หากผ่านการตรวจสอบทั้งหมด ตัวแยกวิเคราะห์จะสร้างโครงสร้างข้อมูลแบบต้นไม้ที่เรียกว่า Abstract Syntax Tree (AST)
  4. AST เป็นโครงสร้างข้อมูลที่แสดงถึงรหัสในโครงสร้างแบบต้นไม้ การเปลี่ยนรหัสเป็นรหัสเครื่องจาก AST นั้นง่ายกว่า
  5. จากนั้นล่ามจะดำเนินการรับ AST และเปลี่ยนเป็น IR ซึ่งเป็นนามธรรมของรหัสเครื่องและเป็นตัวกลางระหว่างรหัส JS และรหัสเครื่อง IR ยังอนุญาตให้ทำการปรับให้เหมาะสมและเคลื่อนที่ได้มากขึ้น
  6. จากนั้นคอมไพเลอร์ JIT จะใช้ IR ที่สร้างขึ้นและเปลี่ยนเป็นรหัสเครื่อง โดยการคอมไพล์โค้ด รับคำติชมในทันที และใช้คำติชมนั้นเพื่อปรับปรุงกระบวนการคอมไพล์

ขอบคุณสำหรับการอ่าน :)

คุณสามารถติดตามฉันได้ที่TwitterและLinkedIn