คู่มือสำหรับผู้เริ่มต้นใช้งาน JavaScript แบบอะซิงโครนัส

อัปเดตบน November 15, 2024 3 นาทีอ่าน

คู่มือสำหรับผู้เริ่มต้นใช้งาน JavaScript แบบอะซิงโครนัส cover image

หากคุณเพิ่งเริ่มต้นการเขียนโปรแกรม มีโอกาสที่คุณกำลังคิดว่าโปรแกรมเป็นชุดของบล็อกตรรกะตามลำดับ โดยแต่ละบล็อกทำหน้าที่เฉพาะและส่งผลลัพธ์เพื่อให้บล็อกถัดไปสามารถรันได้เรื่อยๆ และสำหรับ ส่วนใหญ่คุณพูดถูก โปรแกรมส่วนใหญ่ทำงานตามลำดับ โมเดลนี้ช่วยให้เราสร้างโปรแกรมที่เขียนและบำรุงรักษาได้ง่าย มีกรณีการใช้งานเฉพาะเจาะจงที่โมเดลตามลำดับนี้ใช้งานไม่ได้หรือไม่เหมาะสมที่สุด ตัวอย่างเช่น พิจารณาแอปพลิเคชันการอ่านหนังสือ แอปพลิเคชั่นนี้มีคุณสมบัติขั้นสูงบางอย่าง เช่น การค้นหาคำที่ปรากฏทั้งหมด การนำทางระหว่างบุ๊กมาร์ก และอื่น ๆ ที่คล้ายกัน ทีนี้ ลองนึกภาพผู้ใช้กำลังอ่านหนังสือขนาดยาวและตัดสินใจค้นหาคำทั่วไปเช่น “The” ที่ปรากฏทั้งหมด โดยปกติแอปพลิเคชันจะใช้เวลาสองสามวินาทีในการค้นหาและจัดทำดัชนีคำนั้นทั้งหมด ในโปรแกรมต่อเนื่อง ผู้ใช้ไม่สามารถโต้ตอบกับแอปพลิเคชันได้ (เปลี่ยนหน้าหรือเน้นข้อความ) จนกว่าการดำเนินการค้นหาจะเสร็จสมบูรณ์ หวังว่าคุณจะเห็นได้ว่านั่นไม่ใช่ประสบการณ์การใช้งานที่ดีที่สุด!

1

แผนภาพแสดงขั้นตอนการดำเนินการทั่วไปของแอปพลิเคชันเครื่องอ่านหนังสือ หากผู้ใช้เริ่มต้นการดำเนินการที่ใช้เวลานาน (ในกรณีนี้คือการค้นหาคำว่า “the” ที่เกิดขึ้นทั้งหมดในหนังสือเล่มใหญ่) แอปพลิเคชันจะ “ค้าง” ตลอดระยะเวลาของการดำเนินการนั้น ในกรณีนี้ ผู้ใช้จะคลิกที่ปุ่มบุ๊กมาร์กถัดไปโดยไม่มีผลลัพธ์จนกว่าการดำเนินการค้นหาจะเสร็จสิ้น และการดำเนินการทั้งหมดจะมีผลในทันที ทำให้ผู้ใช้รู้สึกว่าแอปพลิเคชันล่าช้า

คุณอาจสังเกตเห็นว่าตัวอย่างนี้ไม่สอดคล้องกับโมเดลลำดับที่เราแนะนำก่อนหน้านี้จริงๆ เนื่องจากการดำเนินงานที่นี่เป็นอิสระจากกัน ผู้ใช้ไม่จำเป็นต้องทราบจำนวนครั้งของ “the” เพื่อนำทางไปยังบุ๊กมาร์กถัดไป ดังนั้นลำดับการดำเนินการจึงไม่สำคัญนัก เราไม่ต้องรอสิ้นสุดการดำเนินการค้นหาก่อนจึงจะสามารถนำทางไปยังบุ๊กมาร์กถัดไปได้ การปรับปรุงที่เป็นไปได้สำหรับโฟลว์การดำเนินการก่อนหน้านี้จะขึ้นอยู่กับตรรกะนี้: เราสามารถเรียกใช้การดำเนินการค้นหาแบบยาวในเบื้องหลัง ดำเนินการกับการดำเนินการใดๆ ที่เข้ามา และเมื่อการดำเนินการแบบยาวเสร็จสิ้น เราก็สามารถแจ้งให้ผู้ใช้ทราบได้ ขั้นตอนการดำเนินการจะเป็นดังนี้:

2

ด้วยโฟลว์การดำเนินการนี้ ประสบการณ์ผู้ใช้จึงได้รับการปรับปรุงอย่างมาก ขณะนี้ผู้ใช้สามารถเริ่มต้นการดำเนินการที่ใช้เวลานาน ใช้งานแอปพลิเคชันได้ตามปกติ และรับการแจ้งเตือนเมื่อการดำเนินการเสร็จสิ้น นี่คือพื้นฐานของการเขียนโปรแกรมแบบอะซิงโครนัส

นอกเหนือจากภาษาอื่นๆ 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 สำหรับเวิร์กช็อปฟรีครั้งก่อนๆ และติดตามเราบน โซเชียลมีเดีย ดังนั้นคุณจะไม่พลาดกิจกรรมที่กำลังจะมาถึง!

พิสูจน์อาชีพของคุณในอนาคตด้วยการเพิ่มทักษะใน HTML, CSS และ JavaScript ด้วย Code Labs Academy’s Web Development Bootcamp.