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

Jan 27 2023
MDN กำหนดเวิร์กโฟลว์แบบอะซิงโครนัสเป็น ..

MDN กำหนดเวิร์กโฟลว์แบบอะซิงโครนัสเป็น

..การเริ่มงานที่อาจใช้เวลานานและยังคงสามารถตอบสนองต่อเหตุการณ์อื่นๆ ในขณะที่งานนั้นทำงาน... เมื่องานนั้นเสร็จสิ้น โปรแกรมของคุณจะแสดงผลลัพธ์

แต่สิ่งนี้สามารถทำได้บนเว็บด้วย JavaScript ได้อย่างไร

ก่อนที่จะดำดิ่งสู่การใช้งาน async การมีความเข้าใจพื้นฐานของ JS ในระดับที่ต่ำกว่าจะช่วยให้

JS ทำงานอย่างไร

JavaScript เป็น single-threaded และตีความ หมายความว่าคำสั่งถูกดำเนินการโดยตรงและไม่มีขั้นตอนการคอมไพล์แบบ ex-ante ยกตัวอย่างเครื่องยนต์ V8 ซึ่งเป็นเครื่องยนต์ยอดนิยม

ในระดับสูง V8 จะแยกวิเคราะห์โค้ด สร้างโครงสร้างไวยากรณ์แบบนามธรรม (AST) และคอมไพล์ให้เป็น bytecode ที่ปรับให้เหมาะสมด้วยล่าม นอกจากนี้ยังมีตัวเตรียมคำ คอมไพเลอร์สองตัว และแคชที่ทำให้ทำงานได้อย่างรวดเร็ว (V8 มีบล็อก ที่ยอดเยี่ยม เกี่ยวกับเรื่องนี้) มัน ให้เฉพาะ call stack, heap และ event loop ( ไม่ใช่จริงๆ ) เราสามารถเพิกเฉยต่อฮีปสำหรับจุดประสงค์ของรูกระต่ายนี้: มันเป็นหน่วยความจำที่ไม่มีโครงสร้างซึ่งมีการอ้างอิงวัตถุและตัวแปร

วนรอบเหตุการณ์รันไทม์

ซึ่งแตกต่างจาก Java หรือ C++, JS อนุญาตให้ดำเนินการเหตุการณ์หนึ่งครั้ง เราสามารถเห็นสิ่งนี้ในเบราว์เซอร์ของเราโดยติดตามเส้นทางการโทรด้วยconsole.trace():

// shoutout to ES6 arrow functions

foo = () => {
    console.log("Hello, World!");
    bar();
}
bar = () => {
    console.log("Goodbye, World!");
    trace();
}

trace = () => console.trace();

foo();

Hello, World!   VM1049:6
Goodbye, World! VM1049:11 

// console.trace
trace @ VM1049:11
bar @ VM1049:7
foo @ VM1049:3

      
                
call stack visualization

ตั้งค่าหมดเวลา

มาเรียกใช้วิธีการเดียวกันด้วยการหน่วงเวลาแบบอะซิงโครนัส 5,000 มิลลิวินาทีด้วยsetTimeout().

foo = () => {
    console.log("Hello, World!");
     setTimeout(bar, 5000);
}

bar = () => {
    console.log("Goodbye, World!");
    trace();
}

trace = () => console.trace();

foo();

Hello, World!   VM1526:2

// 3 days later... 
Goodbye, World!  VM1526:6

// console.trace 
trace @ VM1382:10
bar @ VM1382:6

// setTimeout is invoked
b @ main-base.bundle.UM3…oFn_e5gQ.12.js:1723
setTimeout (async)  
Cga.window.setTimeout @ main-base.bundle.UM3…toFn_e5gQ.12.js:516

// foo is pushed to the stack first
foo @ VM1382:3

setTimeoutไม่ได้เป็นส่วนหนึ่งของเอ็นจิ้น JS ซึ่งสมเหตุสมผลเนื่องจากเอ็นจิ้นสามารถจัดการเหตุการณ์ได้ครั้งละหนึ่งเหตุการณ์ อย่างไรก็ตาม เบราว์เซอร์เป็นแบบมัลติเธรดและให้ API เว็บแก่เราเพื่อเข้าถึงการใช้งาน setTimeout ตัวอย่างเช่น Chrome ใช้เครื่องมือ V8 แบบเธรดเดียวในสถาปัตยกรรมแบบหลายกระบวนการ ซึ่งทุกกระบวนการเป็นแบบมัลติเธรดอย่างมาก ในกรณีนี้ เราเรียกสิ่งนี้อย่างชัดเจนด้วยCga.setTimeout @ main-base.bundle.blah-blah-blah...Virtually ซึ่งช่วยให้เราสามารถใช้หลายเธรดได้

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

มาจัดลำดับบล็อกรหัสของเราใหม่:

foo = () => {
    console.log("Hello, World!");
     setTimeout(bar, 5000);
}

bar = () => {
    setTimeout(() => console.log("Goodbye, World!"), 5000);
    trace();
}

trace = () => console.trace();

foo();

// output of foo 
Hello, World! VM1649:11 

// console.trace 
trace @ VM1694:11
bar @ VM1694:8
b @ main-base.bundle.UM3…oFn_e5gQ.12.js:1723
setTimeout (async)  
Cga.window.setTimeout @ main-base.bundle.UM3…toFn_e5gQ.12.js:516
foo @ VM1694:3
// end trace 

// 3 days later...
VM1649:6 Goodbye, World!

JRE รวมถึง:

  • JS Engine (เธรดเดียว)
  • Java Virtual Machine (JVM) — เครื่องเสมือนที่รันโค้ดในรันไทม์ Btw นี่คือสาเหตุที่ "VM" เพิ่มบันทึกการติดตาม
  • API เว็บ
  • คิวงาน
  • วนรอบเหตุการณ์

call stack ของเครื่องยนต์ของเรากำลังแสดงเหตุการณ์ทีละรายการ เมื่อเห็น setTimeout() เมธอดพร้อมกับการเรียกกลับ (การอ้างอิงฟังก์ชัน) จะถูกมอบหมายให้กับเว็บ API

โปรดสังเกตว่าเอ็นจิ้นไม่สนใจวิธีที่การใช้งาน setTimeout ของ API จัดการกับเหตุการณ์และการเรียกกลับ เอ็นจิ้นจะดำเนินการต่อเพื่อแสดงเหตุการณ์ถัดไปและป้อนไปยัง Pacman (JVM ที่รันโค้ด)

ในระหว่างนี้ API จะใช้เธรดเพื่อรอ 5,000 มิลลิวินาที แล้วส่งต่อไปยังคิวงาน ลูปเหตุการณ์เห็นว่าเหตุการณ์นั้นเข้าแถวและผลักกลับเข้าไปใน call stack ก็ต่อเมื่อ call stack ว่างเปล่า

คุณสามารถใช้วิชวลไลเซอร์ สนุกๆ ของ Philip Roberts เพื่อดูการทำงานทั้งหมด ลองดูการพูดคุย ที่ยอดเยี่ยมของเขา ที่ JSConf ในปี 2014