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. ตรวจสอบชื่อผู้ใช้และรหัสผ่านของผู้ใช้
  2. รับบทบาทของแอปพลิเคชันสำหรับผู้ใช้
  3. บันทึกเวลาเข้าถึงแอปพลิเคชันสำหรับผู้ใช้

วิธีที่ 1: โทรกลับนรก (“The Pyramid of Doom”)

วิธีแก้ปัญหาแบบโบราณในการซิงโครไนซ์การโทรเหล่านี้คือการเรียกกลับแบบซ้อน นี่เป็นแนวทางที่เหมาะสมสำหรับงาน JavaScript แบบอะซิงโครนัสธรรมดา แต่ไม่สามารถปรับขนาดได้เนื่องจากปัญหาที่เรียกว่า callback hell

ภาพประกอบ: Asynchronous JavaScript callback hell anti-pattern

รหัสสำหรับงานง่าย ๆ สามอย่างจะมีลักษณะดังนี้:

 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 ดั้งเดิม

สัญญาจาวาสคริปต์

สัญญาเป็นขั้นตอนต่อไปในการหลบหนีจากนรกโทรกลับ เมธอดนี้ไม่ได้ลบการใช้การเรียกกลับ แต่ทำให้การโยงฟังก์ชันตรงไปตรงมาและทำให้โค้ดง่ายขึ้น ทำให้อ่านง่ายขึ้นมาก

ภาพประกอบ: แผนภาพสัญญา 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 วันนี้

  1. รหัสที่ได้นั้นสะอาดกว่ามาก
  2. การจัดการข้อผิดพลาดทำได้ง่ายกว่ามากและอาศัยการ try / catch เช่นเดียวกับโค้ดซิงโครนัสอื่น ๆ
  3. การดีบักนั้นง่ายกว่ามาก การตั้งค่าเบรกพอยต์ภายในบล็อก .then จะไม่ย้ายไปที่ .then ถัดไป เนื่องจากเป็นขั้นตอนผ่านโค้ดซิงโครนัสเท่านั้น แต่คุณสามารถดำเนินการ await สายได้เหมือนกับว่าเป็นการโทรแบบซิงโครนัส