JavaScript Under The Hood: วนรอบเหตุการณ์
เรามาฝึกฝน JavaScript ให้เชี่ยวชาญด้วยการสำรวจการทำงานตั้งแต่เริ่มต้น
![](https://post.nghiatu.com/assets/images/m/max/724/1*6m96dStEdZ6dP7j6dX-qjQ.jpeg)
คุณเคยประสบกับข้อผิดพลาดที่ไม่ได้กำหนดหรือมีปัญหาในการระบุขอบเขตของตัวแปรหรือไม่?
ใช้เวลานานมากในการดีบักข้อผิดพลาดใดๆ โดยไม่รู้ว่ารหัสทำงานอย่างไร
ในบล็อกนี้ ฉันจะแสดงให้เห็นว่าแนวคิดขั้นสูง เช่นevent loop
ที่เกี่ยวกับexecution context
, call stack
และcallback queue
ใช้งานได้จริงอย่างไร
ข้อจำกัดความรับผิดชอบ — แนวคิดนี้ต้องใช้ความรู้อย่างมากและเชื่อมโยงถึงกัน ดังนั้นโปรดอย่าแม้แต่จะกระพริบตา!
เครื่องมือจาวาสคริปต์
เครื่องยนต์ V8 ของ Google เป็นภาพประกอบที่รู้จักกันดีของเครื่องยนต์ JavaScript ตัวอย่างเช่น Chrome และ Node.js ต่างก็ใช้เครื่องยนต์ V8 โดยพื้นฐานแล้ว เครื่องยนต์ V8 ประกอบด้วยสองส่วน—
- Call Stack :รหัสทั้งหมดจะถูกดำเนินการภายในนั้น มันทำงานเหมือนกับโครงสร้างข้อมูลแบบ Stack ซึ่งหมายถึงการทำตามแนวคิด LIFO (เข้าก่อนออกก่อน)
- ฮีปหน่วยความจำ:ตำแหน่งที่เกิดการจัดสรรหน่วยความจำ ซึ่งแตกต่างจากโครงสร้างข้อมูลกองมันเป็นเพียงหน่วยความจำ
![](https://post.nghiatu.com/assets/images/m/max/724/1*O1SRNrpq1jc4t10u-cjXsQ.png)
Global Execution Context หรือ GEC คือบริบทการดำเนินการเริ่มต้นที่สร้างโดยเครื่องมือ JavaScript เมื่อใดก็ตามที่ได้รับไฟล์สคริปต์
โค้ด JavaScript ทั้งหมดที่ไม่ได้อยู่ในฟังก์ชันจะทำงานในGEC
. ขั้นตอนต่อไปนี้ดำเนินการในGEC
—
- สร้างพื้นที่หน่วยความจำเพื่อจัดเก็บตัวแปรและฟังก์ชันทั้งหมดในระดับโลก
- สร้างวัตถุส่วนกลาง
- สร้างคำหลัก
this
![](https://post.nghiatu.com/assets/images/m/max/724/1*tCaJpSVnmeST6jV7zKUQYQ.png)
ขึ้นอยู่กับตำแหน่งที่รหัสของคุณจะถูกดำเนินการจะเป็นตัวกำหนดthis
ตำแหน่งที่ตั้ง ตัวอย่างเช่น ใน Node.js จะชี้ไปที่ออบเจ็กต์ส่วนกลางที่แตกต่างกัน ในขณะที่ในเบราว์เซอร์จะชี้ไปที่window
ออบเจ็กต์
![](https://post.nghiatu.com/assets/images/m/max/724/1*aN5ODmv0LsHPgtdMKmp2jw.png)
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');
![](https://post.nghiatu.com/assets/images/m/max/724/1*N-jagOKQ2aJ68byPU71Lsw.png)
ขั้นตอนที่ 2: —ในตัวอย่างของเรา บรรทัดที่ 1 จะถูกดำเนินการซึ่งก็f1
คือ หน่วยความจำจะถูกจัดสรรf1
และจัดเก็บคำจำกัดความของมัน
![](https://post.nghiatu.com/assets/images/m/max/724/1*saeVDitxNhlpitPKKnEy-Q.png)
ขั้นตอนที่ 3: —ในบรรทัดที่ 2 มีการเรียกใช้ฟังก์ชัน สำหรับการเรียกใช้ฟังก์ชันนี้ a Function Execution Context
หรือFEC
จะถูกสร้างขึ้นและจะถูกเก็บไว้ที่ด้านบนCall Stack
ของ
![](https://post.nghiatu.com/assets/images/m/max/724/1*IkpgB-io7nKPVE5kgX6zPw.png)
ขั้นตอนที่ 4: —ตอนนี้ ทั้งหมดf1()
จะถูกดำเนินการทีละบรรทัด และหลังจากเสร็จสิ้นการดำเนินการ มันจะถูกลบออกจากไฟล์Call Stack
.
![](https://post.nghiatu.com/assets/images/m/max/724/1*TlBnvLot48jfgi9SF2Q10A.png)
ขั้นตอนที่ 5: —จากนั้น บรรทัดสุดท้ายconsole.log('end')
จะถูกดำเนินการ และจะพิมพ์ 'end' บนคอนโซล สุดท้าย การรันโค้ดทั้งหมดของคุณGlobal Execution Context
จะถูกลบออกจากไฟล์Call Stack
.
![](https://post.nghiatu.com/assets/images/m/max/724/1*ygugYsbT0g3asVikVznAUA.png)
JS จัดการงานอะซิงโครนัสอย่างไร
อย่างที่เราทราบกันดีว่าจาวาสคริปต์เป็น ภาษาแบบ ซิงโครนัสแบบเธรดเดียว (ทีละงาน) และเธรดแบบเดี่ยวจะcall stack
ทำงานทันทีไม่ว่าจะเกิดอะไรขึ้นก็ตาม
แต่ถ้าเราต้องดำเนินการบางอย่างหลังจาก 5 วินาทีล่ะ เราสามารถทำสิ่งนั้นในcall stack
?
ไม่เราทำไม่ได้ เพราะcall stack
ไม่มีตัวจับเวลา แต่เราจะทำอย่างนั้นได้อย่างไร?
นี่คือที่มา ของ รันไทม์ JavaScript
![](https://post.nghiatu.com/assets/images/m/max/724/1*N_hLdkPpVNnPWl4xFhp03A.png)
อาจเสียใจถ้าฉันบอกคุณตอนนี้ - setTimeout()
ไม่ได้เป็นส่วนหนึ่งของ JavaScript แม้ว่าconsole.log()
หรือเหตุการณ์ DOMทั้งหมดจะเป็นส่วนหนึ่งของWeb APIที่ให้การเข้าถึงJavaScript Engineเพื่อใช้คุณสมบัติทั้งหมดในGlobal Execution Context (GEC)ผ่าน วัตถุwindow
ส่วนกลาง Web APIเหล่านี้เรียกว่าอะซิงโครนัส
Callback Queue คืออะไร?
คิวของงานที่เรียกว่า "คิวการโทรกลับ" หรือ "คิวงาน" คือคิวที่ดำเนินการหลังจากหน้าที่ปัจจุบันของคอลแต็กเสร็จสมบูรณ์แล้ว งานที่ลงทะเบียนในเว็บ API จะย้ายจากเว็บ API ไปยังคิวการเรียกกลับ
คิวการเรียกกลับทำงานเหมือนกับโครงสร้างข้อมูลคิว หมายความว่างานจะถูกจัดการตามลำดับ FIFO (เข้าก่อน ออกก่อน) ซึ่งตรงข้ามกับ call stack ซึ่งหมายความว่างานจะถูกจัดการตามลำดับที่เพิ่มเข้าไปในคิว
![](https://post.nghiatu.com/assets/images/m/max/724/1*_eIkK_aft8TxwPhoakUXRA.png)
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 API
call Back Queue
Event loop
ใส่callBack()
ฟังก์ชันลงในcall Stack
เมื่อเสร็จแล้วทุกอย่างก็ทำงาน และสุดท้ายcallBack()
ฟังก์ชันจะทำงานและพิมพ์ 'โทรกลับ' ลงในคอนโซล
![](https://post.nghiatu.com/assets/images/m/max/724/1*jacQ8ghhz5OgdaUCQq09eA.png)
![](https://post.nghiatu.com/assets/images/m/max/724/1*6tigYelnuSD2A_4SFu6V0w.png)
![](https://post.nghiatu.com/assets/images/m/max/724/1*dIsz0VRqbHG3kDlDjcrsZQ.png)
![](https://post.nghiatu.com/assets/images/m/max/724/1*zGCl4vl40qSkH3QDFaW40w.png)
![](https://post.nghiatu.com/assets/images/m/max/724/1*naxW5_kaQeNB8BNEAf-Uiw.png)
![](https://post.nghiatu.com/assets/images/m/max/724/1*3ltYwQFY3pRUsNeAX81BPQ.png)
![](https://post.nghiatu.com/assets/images/m/max/724/1*S84FJnndsIe31fNMBg2SSQ.png)
![](https://post.nghiatu.com/assets/images/m/max/724/1*mp1TvBDvBIBvkWNIfwLIBg.png)
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 ทำงานอย่างไรในสภาพแวดล้อมรันไทม์
![](https://post.nghiatu.com/assets/images/m/max/724/1*x6gbWPTHXDKgtlA_-l-P0Q.gif)
- ในตอนแรก
GEC
จะถูกสร้างขึ้นภายใน และโค้ดจะถูกดำเนินการ ทีcall stack
ละบรรทัดในGEC
มันเก็บคำจำกัดความของฟังก์ชั่นทั้งหมดในMemory Heap - เมื่อ
main()
มีการเรียก aFunction Execution Context (FEC)
จะถูกสร้างขึ้นและจากนั้นจะเข้าสู่ call stack หลังจากนั้นโค้ดทั้งหมดของmain()
ฟังก์ชันจะถูกดำเนินการทีละบรรทัด - มีบันทึกคอนโซลเพื่อพิมพ์คำหลัก ดังนั้นการ
console.log('main')
ดำเนินการและออกไปจากสแต็ก - API ของ
setTimeout()
เบราว์เซอร์เกิดขึ้น ฟังก์ชันการโทรกลับทำให้อยู่ในคิวการโทรกลับ แต่ในสแต็ก การดำเนินการจะเกิดขึ้นตามปกติ ดังนั้นf2()
เข้าไปในสแต็ก บันทึกคอนโซลของการf2()
ดำเนิน การ ทั้งสองออกไปจากกอง - แล้วมัน
main()
ก็โผล่ออกมาจากสแต็ค - วนรอบเหตุการณ์จะรับรู้ว่า call stack ว่างเปล่า และมีฟังก์ชัน callback อยู่ในคิว ดังนั้นฟังก์ชันการโทรกลับ
f1()
จะถูกใส่ลงในสแต็กโดยไฟล์event loop
. การดำเนินการเริ่มต้นขึ้น บันทึกของคอนโซลดำเนินการ และf1()
เด้งออกจากสแต็กด้วย และสุดท้าย ไม่มีอะไรอยู่ในสแต็กและคิวที่จะดำเนินการต่อไป
มันเป็นการปฏิบัติและบันทึกของฉัน หากคุณพบว่ามีประโยชน์ โปรดแสดงการสนับสนุนของคุณโดยคลิกที่ไอคอนตบมือด้านล่าง ติดตามผมได้ทางสื่อ