JavaScript แบบอะซิงโครนัส: จาก Callback Hell เป็น Async และ Await
เผยแพร่แล้ว: 2022-03-11สิ่งสำคัญอย่างหนึ่งในการเขียนเว็บแอปพลิเคชันให้ประสบความสำเร็จคือสามารถเรียกใช้ AJAX ได้หลายสิบครั้งต่อหน้า
นี่เป็นความท้าทายในการเขียนโปรแกรมแบบอะซิงโครนัสโดยทั่วไป และวิธีที่คุณเลือกจัดการกับการโทรแบบอะซิงโครนัสส่วนใหญ่จะสร้างหรือหยุดแอปของคุณ และโดยการขยายอาจทำให้การเริ่มต้นทั้งหมดของคุณ
การซิงโครไนซ์งานอะซิงโครนัสใน JavaScript เป็นปัญหาร้ายแรงมาเป็นเวลานาน
ความท้าทายนี้ส่งผลกระทบต่อนักพัฒนาส่วนหลังที่ใช้ Node.js มากเท่ากับนักพัฒนาส่วนหน้าที่ใช้เฟรมเวิร์ก JavaScript ใดๆ การเขียนโปรแกรมแบบอะซิงโครนัสเป็นส่วนหนึ่งของงานประจำวันของเรา แต่ความท้าทายมักถูกมองข้ามและไม่ได้รับการพิจารณาในเวลาที่เหมาะสม
ประวัติโดยย่อของ Asychronous JavaScript
วิธีแก้ปัญหาแรกและตรงไปตรงมาที่สุดมาในรูปแบบของ ฟังก์ชันที่ซ้อนกันเป็น callbacks วิธีแก้ปัญหานี้นำไปสู่สิ่งที่เรียกว่า callback hell และแอปพลิเคชั่นจำนวนมากเกินไปยังคงรู้สึกว่าถูกเผาไหม้
จากนั้น เราก็ได้ Promises รูปแบบนี้ทำให้โค้ดอ่านง่ายขึ้นมาก แต่ก็ห่างไกลจากหลักการ Don't Repeat Yourself (DRY) ยังมีอีกหลายกรณีที่คุณต้องทำซ้ำโค้ดเดียวกันเพื่อจัดการโฟลว์ของแอปพลิเคชันอย่างเหมาะสม การเพิ่มล่าสุดในรูปแบบของคำสั่ง async/await ในที่สุดก็สร้างโค้ดแบบอะซิงโครนัสใน JavaScript ให้อ่านและเขียนได้ง่ายเหมือนกับโค้ดอื่นๆ
ลองมาดูตัวอย่างของแต่ละโซลูชันเหล่านี้และสะท้อนถึงวิวัฒนาการของการเขียนโปรแกรมแบบอะซิงโครนัสใน JavaScript
ในการทำเช่นนี้ เราจะตรวจสอบงานง่าย ๆ ที่ทำตามขั้นตอนต่อไปนี้:
- ตรวจสอบชื่อผู้ใช้และรหัสผ่านของผู้ใช้
- รับบทบาทของแอปพลิเคชันสำหรับผู้ใช้
- บันทึกเวลาเข้าถึงแอปพลิเคชันสำหรับผู้ใช้
วิธีที่ 1: โทรกลับนรก (“The Pyramid of Doom”)
วิธีแก้ปัญหาแบบโบราณในการซิงโครไนซ์การโทรเหล่านี้คือการเรียกกลับแบบซ้อน นี่เป็นแนวทางที่เหมาะสมสำหรับงาน JavaScript แบบอะซิงโครนัสธรรมดา แต่ไม่สามารถปรับขนาดได้เนื่องจากปัญหาที่เรียกว่า callback hell
รหัสสำหรับงานง่าย ๆ สามอย่างจะมีลักษณะดังนี้:
const verifyUser = function(username, password, callback){ dataBase.verifyUser(username, password, (error, userInfo) => { if (error) { callback(error) }else{ dataBase.getRoles(username, (error, roles) => { if (error){ callback(error) }else { dataBase.logAccess(username, (error) => { if (error){ callback(error); }else{ callback(null, userInfo, roles); } }) } }) } }) };
แต่ละฟังก์ชันได้รับอาร์กิวเมนต์ซึ่งเป็นอีกฟังก์ชันหนึ่งที่เรียกว่าด้วยพารามิเตอร์ที่ตอบสนองต่อการกระทำก่อนหน้านี้
คนจำนวนมากเกินไปจะประสบกับภาวะสมองหยุดนิ่งเพียงแค่อ่านประโยคข้างต้น การมีแอปพลิเคชันที่มีบล็อกโค้ดที่คล้ายกันหลายร้อยบล็อกจะยิ่งสร้างปัญหาให้กับผู้ที่ดูแลโค้ด แม้ว่าพวกเขาจะเขียนโค้ดเองก็ตาม
ตัวอย่างนี้จะซับซ้อนยิ่งขึ้นเมื่อคุณตระหนักว่า database.getRoles
เป็นอีกฟังก์ชันหนึ่งที่มีการเรียกกลับที่ซ้อนกัน
const getRoles = function (username, callback){ database.connect((connection) => { connection.query('get roles sql', (result) => { callback(null, result); }) }); };
นอกเหนือจากการมีโค้ดที่ดูแลรักษายากแล้ว หลักการ DRY ก็ไม่มีประโยชน์ในกรณีนี้เลย ตัวอย่างเช่น การจัดการข้อผิดพลาดจะเกิดขึ้นซ้ำในแต่ละฟังก์ชัน และมีการเรียกการเรียกกลับหลักจากแต่ละฟังก์ชันที่ซ้อนกัน
การดำเนินการ JavaScript แบบอะซิงโครนัสที่ซับซ้อนมากขึ้น เช่น การวนซ้ำผ่านการโทรแบบอะซิงโครนัส เป็นความท้าทายที่ยิ่งใหญ่กว่า อันที่จริงแล้ว ไม่มีวิธีง่ายๆ ในการทำเช่นนี้กับการโทรกลับ นี่คือเหตุผลที่ไลบรารี JavaScript Promise เช่น Bluebird และ Q ได้รับแรงฉุดอย่างมาก พวกเขาให้วิธีการดำเนินการทั่วไปในคำขอแบบอะซิงโครนัสที่ภาษานั้นไม่ได้จัดเตรียมไว้
นั่นคือที่มาของสัญญา JavaScript ดั้งเดิม
สัญญาจาวาสคริปต์
สัญญาเป็นขั้นตอนต่อไปในการหลบหนีจากนรกโทรกลับ เมธอดนี้ไม่ได้ลบการใช้การเรียกกลับ แต่ทำให้การโยงฟังก์ชันตรงไปตรงมาและทำให้โค้ดง่ายขึ้น ทำให้อ่านง่ายขึ้นมาก

เมื่อใช้งาน Promises โค้ดในตัวอย่าง JavaScript แบบอะซิงโครนัสของเราจะมีลักษณะดังนี้:
const verifyUser = function(username, password) { database.verifyUser(username, password) .then(userInfo => dataBase.getRoles(userInfo)) .then(rolesInfo => dataBase.logAccess(rolesInfo)) .then(finalResult => { //do whatever the 'callback' would do }) .catch((err) => { //do whatever the error handler needs }); };
เพื่อให้เกิดความเรียบง่ายแบบนี้ ทุกฟังก์ชันที่ใช้ในตัวอย่างจะต้องได้รับการ สัญญา มาดูกันว่าเมธอด getRoles
จะได้รับการอัปเดตเพื่อส่งคืน Promise
อย่างไร:
const getRoles = function (username){ return new Promise((resolve, reject) => { database.connect((connection) => { connection.query('get roles sql', (result) => { resolve(result); }) }); }); };
เราได้แก้ไขวิธีการส่งคืน Promise
โดยมีการเรียกกลับสองครั้ง และ Promise
เองก็ดำเนินการจากเมธอดดังกล่าว ตอนนี้ resolve
และ reject
การเรียกกลับจะถูกแมปกับวิธี Promise.then
และ Promise.catch
ตามลำดับ
คุณอาจสังเกตเห็นว่าวิธี getRoles
ยังคงมีแนวโน้มภายในต่อปรากฏการณ์พีระมิดแห่งความหายนะ นี่เป็นเพราะวิธีการสร้างฐานข้อมูลเนื่องจากไม่ส่งคืน Promise
หากวิธีการเข้าถึงฐานข้อมูลของเราส่งคืน Promise
วิธี getRoles
จะมีลักษณะดังนี้:
const getRoles = new function (userInfo) { return new Promise((resolve, reject) => { database.connect() .then((connection) => connection.query('get roles sql')) .then((result) => resolve(result)) .catch(reject) }); };
วิธีที่ 3: Async/Await
พีระมิดแห่งความหายนะได้รับการบรรเทาลงอย่างมากด้วยการแนะนำคำสัญญา อย่างไรก็ตาม เรายังคงต้องพึ่งพาการเรียกกลับที่ส่งต่อไปยังเมธอด .then
และ . .catch
ของ Promise
คำสัญญาปูทางไปสู่หนึ่งในการปรับปรุงที่ยอดเยี่ยมที่สุดใน JavaScript ECMAScript 2017 นำน้ำตาล syntax มาวางทับ Promises ใน JavaScript ในรูปแบบของคำสั่ง async
และ await
สิ่งเหล่านี้ช่วยให้เราสามารถเขียนโค้ดตาม Promise
ราวกับว่ามันเป็นซิงโครนัส แต่ไม่มีการบล็อกเธรดหลัก เนื่องจากตัวอย่างโค้ดนี้สาธิต:
const verifyUser = async function(username, password){ try { const userInfo = await dataBase.verifyUser(username, password); const rolesInfo = await dataBase.getRoles(userInfo); const logStatus = await dataBase.logAccess(userInfo); return userInfo; }catch (e){ //handle errors as needed } };
อนุญาตให้ใช้ Promise
เพื่อแก้ไขได้ภายในฟังก์ชัน async
เท่านั้น ซึ่งหมายความว่า verifyUser
ต้องถูกกำหนดโดยใช้ async function
อย่างไรก็ตาม เมื่อทำการเปลี่ยนแปลงเล็กๆ น้อยๆ นี้แล้ว คุณสามารถ await
Promise
ใดๆ ได้โดยไม่ต้องเปลี่ยนแปลงวิธีอื่นๆ เพิ่มเติม
Async - การแก้ปัญหาที่รอคอยมานาน
ฟังก์ชัน Async เป็นขั้นตอนต่อไปในวิวัฒนาการของการเขียนโปรแกรมแบบอะซิงโครนัสใน JavaScript พวกเขาจะทำให้โค้ดของคุณสะอาดขึ้นและง่ายต่อการบำรุงรักษา การประกาศฟังก์ชันแบบอะซิง async
สจะทำให้มั่นใจได้ว่าฟังก์ชันจะส่งคืน Promise
เสมอ คุณจึงไม่ต้องกังวลเรื่องนั้นอีกต่อไป
เหตุใดคุณจึงควรเริ่มใช้ฟังก์ชัน JavaScript async
วันนี้
- รหัสที่ได้นั้นสะอาดกว่ามาก
- การจัดการข้อผิดพลาดทำได้ง่ายกว่ามากและอาศัยการ
try
/catch
เช่นเดียวกับโค้ดซิงโครนัสอื่น ๆ - การดีบักนั้นง่ายกว่ามาก การตั้งค่าเบรกพอยต์ภายในบล็อก
.then
จะไม่ย้ายไปที่.then
ถัดไป เนื่องจากเป็นขั้นตอนผ่านโค้ดซิงโครนัสเท่านั้น แต่คุณสามารถดำเนินการawait
สายได้เหมือนกับว่าเป็นการโทรแบบซิงโครนัส