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

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


Career Services background pattern

บริการด้านอาชีพ

Contact Section background image

มาติดต่อกันกันเถอะ

Code Labs Academy © 2024 สงวนลิขสิทธิ์.