JavaScript Under The Hood: วนรอบเหตุการณ์

Dec 01 2022
มาเริ่มใช้ JavaScript กันโดยสำรวจการทำงานตั้งแต่เริ่มต้น คุณเคยประสบกับข้อผิดพลาดที่ไม่ได้กำหนดหรือประสบปัญหาในการระบุขอบเขตของตัวแปรหรือไม่? ใช้เวลานานมากในการดีบักข้อผิดพลาดใดๆ โดยไม่รู้ว่ารหัสทำงานอย่างไร ในบล็อกนี้ ฉันจะแสดงให้เห็นว่าแนวคิดขั้นสูง เช่น เหตุการณ์วนรอบเกี่ยวกับบริบทการดำเนินการ การเรียกซ้อน และคิวการเรียกกลับทำงานอย่างไร

เรามาฝึกฝน JavaScript ให้เชี่ยวชาญด้วยการสำรวจการทำงานตั้งแต่เริ่มต้น

ภาพถ่ายโดย Marc St บน Unsplash

คุณเคยประสบกับข้อผิดพลาดที่ไม่ได้กำหนดหรือมีปัญหาในการระบุขอบเขตของตัวแปรหรือไม่?

ใช้เวลานานมากในการดีบักข้อผิดพลาดใดๆ โดยไม่รู้ว่ารหัสทำงานอย่างไร

ในบล็อกนี้ ฉันจะแสดงให้เห็นว่าแนวคิดขั้นสูง เช่นevent loopที่เกี่ยวกับexecution context, call stackและcallback queueใช้งานได้จริงอย่างไร

ข้อจำกัดความรับผิดชอบ — แนวคิดนี้ต้องใช้ความรู้อย่างมากและเชื่อมโยงถึงกัน ดังนั้นโปรดอย่าแม้แต่จะกระพริบตา!

เครื่องมือจาวาสคริปต์

เครื่องยนต์ V8 ของ Google เป็นภาพประกอบที่รู้จักกันดีของเครื่องยนต์ JavaScript ตัวอย่างเช่น Chrome และ Node.js ต่างก็ใช้เครื่องยนต์ V8 โดยพื้นฐานแล้ว เครื่องยนต์ V8 ประกอบด้วยสองส่วน—

  1. Call Stack :รหัสทั้งหมดจะถูกดำเนินการภายในนั้น มันทำงานเหมือนกับโครงสร้างข้อมูลแบบ Stack ซึ่งหมายถึงการทำตามแนวคิด LIFO (เข้าก่อนออกก่อน)
  2. ฮีปหน่วยความจำ:ตำแหน่งที่เกิดการจัดสรรหน่วยความจำ ซึ่งแตกต่างจากโครงสร้างข้อมูลกองมันเป็นเพียงหน่วยความจำ

Global Execution Context หรือ GEC คือบริบทการดำเนินการเริ่มต้นที่สร้างโดยเครื่องมือ JavaScript เมื่อใดก็ตามที่ได้รับไฟล์สคริปต์

โค้ด JavaScript ทั้งหมดที่ไม่ได้อยู่ในฟังก์ชันจะทำงานในGEC. ขั้นตอนต่อไปนี้ดำเนินการในGEC

  • สร้างพื้นที่หน่วยความจำเพื่อจัดเก็บตัวแปรและฟังก์ชันทั้งหมดในระดับโลก
  • สร้างวัตถุส่วนกลาง
  • สร้างคำหลักthis

ขึ้นอยู่กับตำแหน่งที่รหัสของคุณจะถูกดำเนินการจะเป็นตัวกำหนดthisตำแหน่งที่ตั้ง ตัวอย่างเช่น ใน Node.js จะชี้ไปที่ออบเจ็กต์ส่วนกลางที่แตกต่างกัน ในขณะที่ในเบราว์เซอร์จะชี้ไปที่windowออบเจ็กต์

เบราว์เซอร์คอนโซล

Call Stack (หรือ Function Execution Stack)

JavaScript คือsingle-threadedตามที่คุณเคยได้ยินมาแล้ว แต่จริงๆแล้วมันหมายถึงอะไร?

หมายความว่าเอ็นจิ้น JavaScript มีเพียงหนึ่งcall stackหรือfunction execution stack.

  • ดังที่เราทราบ เมื่อใดก็ตามที่คอมไพเลอร์สำรวจโค้ดของคุณเป็นครั้งแรก เอ็นจิ้น JS จะถูกขอให้สร้างGlobal Execution ContextหรือGECโดยคอมไพเลอร์ รวมทั้งขอให้วางลงในไฟล์Call Stack.
  • รหัสทั้งหมดของคุณถูกดำเนินการทีละรหัสในGlobal Execution Contextและจัดสรรหน่วยความจำสำหรับการกำหนดฟังก์ชันหรือการประกาศตัวแปร และเก็บไว้ที่นั่น
  • แต่เมื่อพบการเรียกใช้ฟังก์ชัน a Functional Execution ContextหรือFECถูกสร้างขึ้นเพื่อเรียกใช้โค้ดของฟังก์ชัน จากนั้นจึงเพิ่มไปที่ด้านบนสุดของcall stack.
  • ล่ามจะลบฟังก์ชันออกจากcall stackทุกครั้งที่ฟังก์ชันยุติ ฟังก์ชันยุติ — เมื่อถึงจุดสิ้นสุดของขอบเขตหรือคำสั่งส่งคืน
  • สุดท้าย การรันโค้ดทั้งหมดของคุณGECจะถูกลบออกจากไฟล์Call Stack.

ไม่ต้องกังวล เรามาสาธิตด้วยตัวอย่างกัน

function f1{
  console.log('f1');
}

f1();
console.log('end');

ขั้นตอนที่ 2: —ในตัวอย่างของเรา บรรทัดที่ 1 จะถูกดำเนินการซึ่งก็f1คือ หน่วยความจำจะถูกจัดสรรf1และจัดเก็บคำจำกัดความของมัน

ขั้นตอนที่ 3: —ในบรรทัดที่ 2 มีการเรียกใช้ฟังก์ชัน สำหรับการเรียกใช้ฟังก์ชันนี้ a Function Execution Context หรือFECจะถูกสร้างขึ้นและจะถูกเก็บไว้ที่ด้านบนCall Stackของ

ขั้นตอนที่ 4: —ตอนนี้ ทั้งหมดf1()จะถูกดำเนินการทีละบรรทัด และหลังจากเสร็จสิ้นการดำเนินการ มันจะถูกลบออกจากไฟล์Call Stack.

ขั้นตอนที่ 5: —จากนั้น บรรทัดสุดท้ายconsole.log('end')จะถูกดำเนินการ และจะพิมพ์ 'end' บนคอนโซล สุดท้าย การรันโค้ดทั้งหมดของคุณGlobal Execution Contextจะถูกลบออกจากไฟล์Call Stack.

JS จัดการงานอะซิงโครนัสอย่างไร

อย่างที่เราทราบกันดีว่าจาวาสคริปต์เป็น ภาษาแบบ ซิงโครนัแบบเธรดเดียว (ทีละงาน) และเธรดแบบเดี่ยวจะcall stackทำงานทันทีไม่ว่าจะเกิดอะไรขึ้นก็ตาม

แต่ถ้าเราต้องดำเนินการบางอย่างหลังจาก 5 วินาทีล่ะ เราสามารถทำสิ่งนั้นในcall stack?

ไม่เราทำไม่ได้ เพราะcall stackไม่มีตัวจับเวลา แต่เราจะทำอย่างนั้นได้อย่างไร?

นี่คือที่มา ของ รันไทม์ JavaScript

สภาพแวดล้อมรันไทม์ของ JavaScript

อาจเสียใจถ้าฉันบอกคุณตอนนี้ - setTimeout()ไม่ได้เป็นส่วนหนึ่งของ JavaScript แม้ว่าconsole.log()หรือเหตุการณ์ DOMทั้งหมดจะเป็นส่วนหนึ่งของWeb APIที่ให้การเข้าถึงJavaScript Engineเพื่อใช้คุณสมบัติทั้งหมดในGlobal Execution Context (GEC)ผ่าน วัตถุwindowส่วนกลาง Web APIเหล่านี้เรียกว่าอะซิงโครนั

Callback Queue คืออะไร?

คิวของงานที่เรียกว่า "คิวการโทรกลับ" หรือ "คิวงาน" คือคิวที่ดำเนินการหลังจากหน้าที่ปัจจุบันของคอลแต็กเสร็จสมบูรณ์แล้ว งานที่ลงทะเบียนในเว็บ API จะย้ายจากเว็บ API ไปยังคิวการเรียกกลับ

คิวการเรียกกลับทำงานเหมือนกับโครงสร้างข้อมูลคิว หมายความว่างานจะถูกจัดการตามลำดับ FIFO (เข้าก่อน ออกก่อน) ซึ่งตรงข้ามกับ call stack ซึ่งหมายความว่างานจะถูกจัดการตามลำดับที่เพิ่มเข้าไปในคิว

โทรกลับคิว

Event Loop คืออะไร?

ลูปเหตุการณ์ JavaScript เพิ่มงานจากคิวการโทรกลับไปยัง call stack ตามลำดับ FIFO ทันทีที่ call stack ว่างเปล่า

ลูปเหตุการณ์จะถูกบล็อกหาก call stack กำลังรันโค้ดบางอย่างอยู่ และจะไม่เพิ่มการโทรเพิ่มเติมจากคิวจนกว่า stack จะว่างเปล่าอีกครั้ง นี่เป็นเพราะโค้ด JavaScript ทำงานแบบรันจนจบ

ลองทำความเข้าใจกับแนวคิดข้างต้นด้วยตัวอย่าง

  • ในตอนแรกGlobal Execution Contextถูกสร้างขึ้นสำหรับรหัสของเราภายในของเราcall stackและGECดำเนินการบรรทัดรหัสของเราทีละบรรทัด
  • GECรันบรรทัดที่ 1 และพิมพ์ 'Start' บนคอนโซลของเรา
  • เมื่อดำเนินการบรรทัดที่ 2 setTimeout()เว็บ API จะถูกเรียก จากนั้นจึงsetTimeout()ให้สิทธิ์เข้าถึงคุณสมบัติตัวจับเวลา จากนั้นคุณจะสามารถตั้ง5000msเวลาหน่วงเวลาได้
  • เมื่อคุณส่งcallBack()ฟังก์ชันผ่าน ฟังก์ชันsetTimeout()นั้นcallBack()จะถูกลงทะเบียนเป็น Call Back over web Web API
  • จากนั้นGECดำเนินการบรรทัดที่ 1 รวมทั้งพิมพ์ 'End' บนคอนโซล
  • เมื่อโค้ดทั้งหมดถูกดำเนินการGECจะถูกลบออกจากcall stackไฟล์ .
  • หลังจากฟังก์ชันที่ลงทะเบียนใน5000 millisecondจะถูกย้ายภายในcallBack()web APIcall Back Queue
  • Event loopใส่callBack()ฟังก์ชันลงในcall Stackเมื่อเสร็จแล้วทุกอย่างก็ทำงาน และสุดท้ายcallBack()ฟังก์ชันจะทำงานและพิมพ์ 'โทรกลับ' ลงในคอนโซล

function f1() {
    console.log('f1');
}

function f2() {
    console.log('f2');
}

function main() {
    console.log('main');
    
    setTimeout(f1, 0);
    
    f2();
}

main();

หากคุณคิดว่า "f1" จะพิมพ์ก่อน "f2" แสดงว่าคุณคิดผิด มันจะเป็น -

main
f2
f1

กลไกที่กล่าวถึงของ JavaScript เหมาะสำหรับฟังก์ชันการเรียกกลับหรือคำขอ API

ลองสังเกตทีละขั้นตอนว่าตัวอย่างที่ 2 ทำงานอย่างไรในสภาพแวดล้อมรันไทม์

  1. ในตอนแรกGECจะถูกสร้างขึ้นภายใน และโค้ดจะถูกดำเนินการ ทีcall stackละบรรทัดใน GECมันเก็บคำจำกัดความของฟังก์ชั่นทั้งหมดในMemory Heap
  2. เมื่อmain()มีการเรียก a Function Execution Context (FEC)จะถูกสร้างขึ้นและจากนั้นจะเข้าสู่ call stack หลังจากนั้นโค้ดทั้งหมดของmain()ฟังก์ชันจะถูกดำเนินการทีละบรรทัด
  3. มีบันทึกคอนโซลเพื่อพิมพ์คำหลัก ดังนั้นการconsole.log('main')ดำเนินการและออกไปจากสแต็ก
  4. API ของsetTimeout()เบราว์เซอร์เกิดขึ้น ฟังก์ชันการโทรกลับทำให้อยู่ในคิวการโทรกลับ แต่ในสแต็ก การดำเนินการจะเกิดขึ้นตามปกติ ดังนั้นf2()เข้าไปในสแต็ก บันทึกคอนโซลของการf2()ดำเนิน การ ทั้งสองออกไปจากกอง
  5. แล้วมันmain()ก็โผล่ออกมาจากสแต็ค
  6. วนรอบเหตุการณ์จะรับรู้ว่า call stack ว่างเปล่า และมีฟังก์ชัน callback อยู่ในคิว ดังนั้นฟังก์ชันการโทรกลับf1()จะถูกใส่ลงในสแต็กโดยไฟล์event loop. การดำเนินการเริ่มต้นขึ้น บันทึกของคอนโซลดำเนินการ และf1()เด้งออกจากสแต็กด้วย และสุดท้าย ไม่มีอะไรอยู่ในสแต็กและคิวที่จะดำเนินการต่อไป

มันเป็นการปฏิบัติและบันทึกของฉัน หากคุณพบว่ามีประโยชน์ โปรดแสดงการสนับสนุนของคุณโดยคลิกที่ไอคอนตบมือด้านล่าง ติดตามผมได้ทางสื่อ