หากคุณเพิ่งเริ่มต้นการเขียนโปรแกรม มีโอกาสที่คุณกำลังคิดว่าโปรแกรมเป็นชุดของบล็อกตรรกะตามลำดับ โดยแต่ละบล็อกทำหน้าที่เฉพาะและส่งผลลัพธ์เพื่อให้บล็อกถัดไปสามารถรันได้เรื่อยๆ และสำหรับ ส่วนใหญ่คุณพูดถูก โปรแกรมส่วนใหญ่ทำงานตามลำดับ โมเดลนี้ช่วยให้เราสร้างโปรแกรมที่เขียนและบำรุงรักษาได้ง่าย มีกรณีการใช้งานเฉพาะเจาะจงที่โมเดลตามลำดับนี้ใช้งานไม่ได้หรือไม่เหมาะสมที่สุด ตัวอย่างเช่น พิจารณาแอปพลิเคชันการอ่านหนังสือ แอปพลิเคชั่นนี้มีคุณสมบัติขั้นสูงบางอย่าง เช่น การค้นหาคำทุกคำ การนำทางระหว่างบุ๊กมาร์ก และอื่น ๆ ที่คล้ายกัน ทีนี้ ลองนึกภาพผู้ใช้กำลังอ่านหนังสือขนาดยาวและตัดสินใจค้นหาคำทั่วไปเช่น "The" ที่ปรากฏทั้งหมด โดยปกติแอปพลิเคชันจะใช้เวลาสองสามวินาทีในการค้นหาและจัดทำดัชนีคำนั้นทั้งหมด ในโปรแกรมต่อเนื่อง ผู้ใช้ไม่สามารถโต้ตอบกับแอปพลิเคชันได้ (เปลี่ยนหน้าหรือเน้นข้อความ) จนกว่าการดำเนินการค้นหาจะเสร็จสมบูรณ์ หวังว่าคุณจะเห็นได้ว่านั่นไม่ใช่ประสบการณ์การใช้งานที่ดีที่สุด!
แผนภาพแสดงขั้นตอนการดำเนินการทั่วไปของแอปพลิเคชันเครื่องอ่านหนังสือ หากผู้ใช้เริ่มต้นการดำเนินการที่ใช้เวลานาน (ในกรณีนี้คือการค้นหาคำว่า "the" ในหนังสือเล่มใหญ่) แอปพลิเคชันจะ "ค้าง" ตลอดระยะเวลาของการดำเนินการนั้น ในกรณีนี้ ผู้ใช้จะคลิกที่ปุ่มบุ๊กมาร์กถัดไปโดยไม่มีผลลัพธ์จนกว่าการดำเนินการค้นหาจะเสร็จสิ้น และการดำเนินการทั้งหมดจะมีผลในทันที ทำให้ผู้ใช้รู้สึกว่าแอปพลิเคชันล่าช้า
คุณอาจสังเกตเห็นว่าตัวอย่างนี้ไม่สอดคล้องกับโมเดลลำดับที่เราแนะนำก่อนหน้านี้จริงๆ เนื่องจากการดำเนินงานที่นี่เป็นอิสระจากกัน ผู้ใช้ไม่จำเป็นต้องทราบจำนวนครั้งของ "the" เพื่อนำทางไปยังบุ๊กมาร์กถัดไป ดังนั้นลำดับการดำเนินการจึงไม่สำคัญนัก เราไม่ต้องรอสิ้นสุดการดำเนินการค้นหาก่อนจึงจะสามารถนำทางไปยังบุ๊กมาร์กถัดไปได้ การปรับปรุงที่เป็นไปได้สำหรับโฟลว์การดำเนินการก่อนหน้านี้จะขึ้นอยู่กับตรรกะนี้: เราสามารถเรียกใช้การดำเนินการค้นหาแบบยาวในเบื้องหลัง ดำเนินการกับการดำเนินการใดๆ ที่เข้ามา และเมื่อการดำเนินการแบบยาวเสร็จสิ้น เราก็สามารถแจ้งให้ผู้ใช้ทราบได้ ขั้นตอนการดำเนินการจะเป็นดังนี้:
ด้วยโฟลว์การดำเนินการนี้ ประสบการณ์ผู้ใช้จึงได้รับการปรับปรุงอย่างมาก ขณะนี้ผู้ใช้สามารถเริ่มต้นการดำเนินการที่ใช้เวลานาน ใช้งานแอปพลิเคชันได้ตามปกติ และรับการแจ้งเตือนเมื่อการดำเนินการเสร็จสิ้น นี่คือพื้นฐานของการเขียนโปรแกรมแบบอะซิงโครนัส
นอกเหนือจากภาษาอื่นๆ Javascript ยังรองรับการเขียนโปรแกรมแบบอะซิงโครนัสสไตล์นี้ด้วยการจัดหา API ที่กว้างขวางเพื่อให้บรรลุถึงพฤติกรรมแบบอะซิงโครนัสแทบทุกรูปแบบที่คุณคิดได้ ท้ายที่สุดแล้ว Javascript ควรเป็นภาษาอะซิงโครนัสโดยธรรมชาติ หากเราอ้างถึงตัวอย่างก่อนหน้านี้ ตรรกะอะซิงโครนัสอยู่ที่ฐานของแอปพลิเคชันการโต้ตอบกับผู้ใช้ทั้งหมด และ Javascript ถูกสร้างขึ้นเพื่อใช้บนเบราว์เซอร์เป็นหลัก ซึ่งโปรแกรมส่วนใหญ่เกี่ยวกับการตอบสนองต่อการกระทำของผู้ใช้
ต่อไปนี้จะให้คำแนะนำโดยย่อเกี่ยวกับ Asynchronous Javascript:
โทรกลับ
ในโปรแกรมทั่วไป คุณมักจะพบฟังก์ชันต่างๆ มากมาย หากต้องการใช้ฟังก์ชัน เราจะเรียกฟังก์ชันนั้นด้วยชุดพารามิเตอร์ โค้ดฟังก์ชันจะดำเนินการและส่งกลับผลลัพธ์ ไม่มีอะไรผิดปกติ การเขียนโปรแกรมแบบอะซิงโครนัสจะเปลี่ยนตรรกะนี้เล็กน้อย ย้อนกลับไปที่ตัวอย่างแอปพลิเคชันอ่านหนังสือ เราไม่สามารถใช้ฟังก์ชันปกติเพื่อใช้ตรรกะการดำเนินการค้นหาได้ เนื่องจากการดำเนินการใช้เวลานานไม่ทราบจำนวน โดยทั่วไปฟังก์ชันปกติจะกลับมาก่อนที่การดำเนินการจะเสร็จสิ้น และนี่ไม่ใช่ลักษณะการทำงานที่เราคาดหวัง วิธีแก้ไขคือการระบุฟังก์ชันอื่นที่จะดำเนินการเมื่อการดำเนินการค้นหาเสร็จสิ้น นี่เป็นการจำลองกรณีการใช้งานของเรา เนื่องจากโปรแกรมของเราสามารถดำเนินการต่อไปได้ตามปกติ และเมื่อการค้นหาเสร็จสิ้น ฟังก์ชันที่ระบุจะดำเนินการเพื่อแจ้งให้ผู้ใช้ทราบถึงผลการค้นหา ฟังก์ชันนี้คือสิ่งที่เราเรียกว่าฟังก์ชันโทรกลับ:
// Search occurrences function
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in result variable
//....
callback(null, word, result);
} catch (err) {
callback(err);
}
}
// Search occurrences callback function
function handleSearchOccurrencesResult(err, word, result) {
if (err) {
console.log(`Search operation for ${word} ended with an error`);
} else console.log(`Search results for ${word}: ${result}`);
return;
}
searchOccurrences("the", handleSearchOccurrencesResult);
ขั้นแรก เรากำหนดฟังก์ชันการดำเนินการค้นหา searchOccurrences ใช้คำในการค้นหาและพารามิเตอร์ตัวที่สอง "callback" ซึ่งจะเป็นฟังก์ชันที่จะดำเนินการเมื่อการดำเนินการค้นหาเสร็จสิ้น ฟังก์ชั่นการดำเนินการค้นหาถูกตั้งใจให้เป็นนามธรรม เราเพียงแต่ต้องมุ่งเน้นไปที่ผลลัพธ์ที่เป็นไปได้สองประการ: กรณีแรกคือที่ทุกอย่างประสบความสำเร็จและเราจะได้ผลลัพธ์ของการค้นหาในตัวแปรผลลัพธ์ ในกรณีนี้ เราเพียงแค่ต้องเรียกฟังก์ชันการเรียกกลับด้วยพารามิเตอร์ต่อไปนี้: พารามิเตอร์แรกเป็นโมฆะหมายความว่าไม่มีข้อผิดพลาดเกิดขึ้น พารามิเตอร์ที่สองคือคำที่ถูกค้นหา และพารามิเตอร์ที่สามและอาจสำคัญที่สุดของทั้งสาม คือผลลัพธ์ของการดำเนินการค้นหา
กรณีที่สองคือกรณีที่มีข้อผิดพลาดเกิดขึ้น นี่เป็นกรณีที่การดำเนินการค้นหาเสร็จสิ้นและเราต้องเรียกใช้ฟังก์ชันการโทรกลับ เราใช้ try and catch block เพื่อสกัดกั้นข้อผิดพลาดใดๆ และเราเพียงแค่เรียกใช้ฟังก์ชัน callback พร้อมกับวัตถุข้อผิดพลาดจาก catch block
จากนั้นเรากำหนดฟังก์ชันการโทรกลับ handleSearchOccurrences เราเก็บตรรกะไว้ค่อนข้างง่าย มันเป็นเพียงเรื่องของการพิมพ์ข้อความไปยังคอนโซล ก่อนอื่นเราจะตรวจสอบพารามิเตอร์ "err" เพื่อดูว่ามีข้อผิดพลาดเกิดขึ้นในฟังก์ชันหลักหรือไม่ ในกรณีนั้น เราเพียงแจ้งให้ผู้ใช้ทราบว่าการดำเนินการค้นหาสิ้นสุดลงด้วยข้อผิดพลาด หากไม่มีข้อผิดพลาดเกิดขึ้น เราจะพิมพ์ข้อความพร้อมผลลัพธ์ของการดำเนินการค้นหา
สุดท้าย เราเรียกฟังก์ชัน searchOccurrences ด้วยคำว่า "the" ตอนนี้ฟังก์ชั่นจะทำงานได้ตามปกติโดยไม่ปิดกั้นโปรแกรมหลัก และเมื่อการค้นหาเสร็จสิ้น การโทรกลับจะถูกดำเนินการและเราจะได้รับข้อความผลลัพธ์พร้อมกับผลการค้นหาหรือข้อความแสดงข้อผิดพลาด
สิ่งสำคัญที่ต้องกล่าวถึงในที่นี้ว่าเรามีสิทธิ์เข้าถึงเฉพาะตัวแปรผลลัพธ์ภายในฟังก์ชันหลักและฟังก์ชันโทรกลับเท่านั้น หากเราลองทำสิ่งนี้:
let result;
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in searchResult variable
//....
result = searchResult;
callback(null, word, result);
} catch (err) {
callback(err);
}
}
searchOccurrences("the", handleSearchOccurrencesResult);
console.log(result);
ผลลัพธ์ของการพิมพ์จะไม่ถูกกำหนดเนื่องจากโปรแกรมไม่รอให้ฟังก์ชัน searchOccurrences ดำเนินการ มันจะย้ายไปยังคำสั่งถัดไปซึ่งเป็นคำสั่งการพิมพ์ก่อนที่จะกำหนดตัวแปรผลลัพธ์ภายในฟังก์ชันหลัก ด้วยเหตุนี้ เราจะพิมพ์ตัวแปรผลลัพธ์ที่ยังไม่ได้กำหนด
ตามตรรกะนี้ เราควรเก็บโค้ดทั้งหมดที่ใช้ตัวแปรผลลัพธ์ไว้ในฟังก์ชันเรียกกลับ สิ่งนี้อาจดูเหมือนไม่เป็นปัญหาในขณะนี้ แต่อาจบานปลายไปสู่ปัญหาที่แท้จริงได้อย่างรวดเร็ว ลองนึกภาพกรณีที่เรามีฟังก์ชันแบบอะซิงโครนัสที่ต่อเนื่องกันซึ่งจำเป็นต้องทำงานตามลำดับ ในลอจิกการโทรกลับทั่วไป คุณจะใช้บางอย่างดังนี้:
functionA(function (err, resA) {
///......
functionB(resA, function (err, resB) {
///......
functionC(resB, function (err, resC) {
///......
functionD(resC, function (err, resD) {
///......
});
});
});
});
โปรดทราบว่าการโทรกลับแต่ละครั้งมีพารามิเตอร์ข้อผิดพลาด และข้อผิดพลาดแต่ละรายการจะต้องได้รับการจัดการแยกกัน สิ่งนี้ทำให้โค้ดที่ซับซ้อนอยู่แล้วข้างต้นซับซ้อนยิ่งขึ้นและยากต่อการดูแลรักษา หวังว่า Promises อยู่ที่นี่เพื่อแก้ไขปัญหาการโทรกลับนรก เราจะพูดถึงเรื่องนั้นในครั้งต่อไป
สัญญา
สัญญาถูกสร้างขึ้นจากการติดต่อกลับและดำเนินการในลักษณะเดียวกัน ได้รับการแนะนำโดยเป็นส่วนหนึ่งของฟีเจอร์ ES6 เพื่อแก้ไขปัญหาบางประการเกี่ยวกับการเรียกกลับ เช่น callback hell คำสัญญาจัดเตรียมฟังก์ชันของตัวเองที่ทำงานเมื่อสำเร็จ (แก้ไข) และเมื่อเกิดข้อผิดพลาด (ปฏิเสธ) ต่อไปนี้จะแสดงตัวอย่าง searchOccurrences ที่นำมาใช้พร้อมกับสัญญา:
// Search occurrences function
function searchOccurrences(word) {
return new Promise((resolve, reject) => {
try {
// search operation logic, result is in result variable
//....
resolve(word, result);
} catch (err) {
reject(err);
}
});
}
searchOccurrences("the")
.then((word, result) => {
console.log(`Search results for ${word}: ${result}`);
})
.catch((err) => {
console.log(`Search operation ended with an error`);
});
มาดูการเปลี่ยนแปลงที่เราใช้กัน:
ฟังก์ชัน searchOccurrences ส่งคืนสัญญา ภายในสัญญา เราคงตรรกะเดียวกัน: เรามีสองฟังก์ชันแก้ไขและปฏิเสธที่แสดงถึงการโทรกลับของเรา แทนที่จะมีฟังก์ชันโทรกลับเดียวที่จัดการทั้งการดำเนินการที่สำเร็จและการดำเนินการที่มีข้อผิดพลาด สัญญาจะแยกผลลัพธ์ทั้งสองออกและให้ไวยากรณ์ที่ชัดเจนเมื่อเรียกใช้ฟังก์ชันหลัก ฟังก์ชันการแก้ไขจะ "เชื่อมต่อ" กับฟังก์ชันหลักโดยใช้คีย์เวิร์ด "แล้ว" ที่นี่เราเพียงระบุพารามิเตอร์สองตัวของฟังก์ชันการแก้ไขและพิมพ์ผลการค้นหา สิ่งที่คล้ายกันนี้ใช้กับฟังก์ชันการปฏิเสธ ซึ่งสามารถเชื่อมต่อได้โดยใช้คีย์เวิร์ด "catch" หวังว่าคุณคงจะพอใจกับข้อดีที่สัญญาไว้ในแง่ของความสามารถในการอ่านโค้ดและความสะอาด หากคุณยังคงถกเถียงกันอยู่ ลองดูวิธีที่เราสามารถแก้ไขปัญหา callback hell โดยการโยงฟังก์ชันอะซิงโครนัสเข้าด้วยกันเพื่อทำงานทีละฟังก์ชัน:
searchOccurrences("the")
.then(searchOccurrences("asynchronous"))
.then(searchOccurrences("javascript"))
.then(searchOccurrences("guide"))
.catch((err) => {
console.log(`Search operation ended with an error`);
});
อะซิงก์/รอสักครู่
Async/Await เป็นส่วนเสริมล่าสุดในแถบเครื่องมืออะซิงโครนัสใน Javascript เปิดตัวด้วย ES8 โดยมอบเลเยอร์ใหม่ของนามธรรมเหนือฟังก์ชันอะซิงโครนัสโดยเพียงแค่ "รอ" สำหรับการดำเนินการของการดำเนินการแบบอะซิงโครนัส โฟลว์ของโปรแกรมบล็อกที่คำสั่งนั้นจนกระทั่งผลลัพธ์ถูกส่งกลับจากการดำเนินการแบบอะซิงโครนัส จากนั้นโปรแกรมจะดำเนินการตามคำสั่งถัดไป หากคุณกำลังคิดถึงโฟลว์การดำเนินการแบบซิงโครนัส แสดงว่าคุณคิดถูกแล้ว เรามาครบวงแล้ว! Async/await พยายามที่จะนำความเรียบง่ายของการเขียนโปรแกรมแบบซิงโครนัสมาสู่โลกอะซิงโครนัส โปรดทราบว่าสิ่งนี้จะรับรู้ได้ในการดำเนินการและโค้ดของโปรแกรมเท่านั้น ทุกอย่างยังคงเหมือนเดิม Async/await ยังคงใช้คำสัญญา และการเรียกกลับเป็นองค์ประกอบหลัก
มาดูตัวอย่างของเราและนำไปใช้โดยใช้ Async/await:
async function searchOccurrences(word) {
try {
// search operation logic, result is in result variable
//....
return result;
} catch (err) {
console.log(`Search operation for ${word} ended with an error`);
}
}
const word = "the";
const result = await searchOccurrences(word, handleSearchOccurrencesResult);
console.log(`Search results for ${word}: ${result}`);
โค้ดของเราไม่ได้เปลี่ยนแปลงมากนัก สิ่งสำคัญที่ต้องสังเกตที่นี่คือคีย์เวิร์ด "async" ก่อนการประกาศฟังก์ชัน searchOccurrences สิ่งนี้บ่งชี้ว่าฟังก์ชันเป็นแบบอะซิงโครนัส นอกจากนี้ ให้สังเกตคีย์เวิร์ด "รอ" เมื่อเรียกใช้ฟังก์ชัน searchOccurrences ซึ่งจะสั่งให้โปรแกรมรอการทำงานของฟังก์ชันจนกว่าผลลัพธ์จะถูกส่งกลับก่อนที่โปรแกรมจะสามารถย้ายไปยังคำสั่งถัดไปได้ กล่าวคือ ตัวแปรผลลัพธ์จะเก็บค่าที่ส่งคืนของฟังก์ชัน searchOccurrences ไว้เสมอ ไม่ใช่คำสัญญาของ ฟังก์ชันในแง่นั้น Async/Await ไม่มีสถานะรอดำเนินการตามสัญญา เมื่อดำเนินการเสร็จสิ้น เราจะย้ายไปที่คำสั่งการพิมพ์ และคราวนี้ผลลัพธ์จะประกอบด้วยผลลัพธ์ของการดำเนินการค้นหาจริงๆ ตามที่คาดไว้ โค้ดใหม่มีลักษณะการทำงานเหมือนกับว่ามันเป็นแบบซิงโครนัส
สิ่งเล็กๆ น้อยๆ อีกประการหนึ่งที่ต้องจำไว้ก็คือ เนื่องจากเราไม่มีฟังก์ชันการโทรกลับอีกต่อไป เราจึงต้องจัดการกับข้อผิดพลาด searchOccurrences ภายในฟังก์ชันเดียวกัน เนื่องจากเราไม่สามารถเผยแพร่ข้อผิดพลาดไปยังฟังก์ชันการโทรกลับและจัดการที่นั่นได้ ที่นี่เราเพียงแต่พิมพ์ข้อความแสดงข้อผิดพลาดในกรณีที่เกิดข้อผิดพลาดเพื่อประโยชน์ของตัวอย่าง
สรุปแล้ว
ในบทความนี้ เราได้อธิบายวิธีการต่างๆ ที่ใช้ในการนำตรรกะอะซิงโครนัสไปใช้งานใน Javascript เราเริ่มต้นด้วยการสำรวจตัวอย่างที่เป็นรูปธรรมว่าเหตุใดเราจึงต้องเปลี่ยนจากรูปแบบการเขียนโปรแกรมซิงโครนัสปกติไปเป็นโมเดลอะซิงโครนัส จากนั้นเราย้ายไปที่การโทรกลับซึ่งเป็นองค์ประกอบหลักของ Javascript แบบอะซิงโครนัส ข้อจำกัดของการโทรกลับนำเราไปสู่ทางเลือกต่างๆ ที่เพิ่มเข้ามาในช่วงหลายปีที่ผ่านมาเพื่อเอาชนะข้อจำกัดเหล่านี้ ซึ่งส่วนใหญ่เป็นคำสัญญาและ Async/await ตรรกะแบบอะซิงโครนัสสามารถพบได้ทุกที่บนเว็บ ไม่ว่าคุณจะเรียกใช้ API ภายนอก เริ่มการสืบค้นฐานข้อมูล เขียนลงในระบบไฟล์ในเครื่อง หรือแม้แต่รอการป้อนข้อมูลของผู้ใช้ในแบบฟอร์มการเข้าสู่ระบบ หวังว่าตอนนี้คุณรู้สึกมั่นใจมากขึ้นที่จะจัดการกับปัญหาเหล่านี้ด้วยการเขียน Asynchronous Javascript ที่สะอาดและบำรุงรักษาได้!
หากคุณชอบบทความนี้ โปรดอ่าน บล็อก CLA ที่เราพูดคุยกันในหัวข้อต่างๆ เกี่ยวกับการเข้าสู่เทคโนโลยี นอกจากนี้ โปรดดู ช่อง YouTube สำหรับเวิร์กช็อปฟรีครั้งก่อนๆ และติดตามเราบน โซเชียลมีเดีย ดังนั้นคุณจะไม่พลาดกิจกรรมที่กำลังจะมาถึง!