Js-async-await-gemini

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา

JavaScript Async/Await: จาก Callback สู่ความทันสมัย. การเขียนโปรแกรมแบบ Asynchronous (อะซิงโครนัส) เป็นหัวใจสำคัญของการทำเว็บแอปพลิเคชันให้ลื่นไหล ในบทความนี้เราจะมาทำความเข้าใจว่าทำไมเราถึงต้องใช้มัน และวิวัฒนาการจากอดีตจนถึงปัจจุบันคือ `async/await`

ธรรมชาติของ JavaScript: Single-Threaded

JavaScript เป็นภาษาแบบ Single-Threaded หมายความว่ามันมี "สมอง" หรือ Call Stack เพียงอันเดียว ทำงานได้ทีละคำสั่ง (One thing at a time)

สมมติว่าคุณเปิดร้านอาหารตามสั่งที่มีพ่อครัวคนเดียว:

  • Synchronous (แบบปกติ): ลูกค้า A สั่งกะเพรา -> พ่อครัวทำกะเพรา -> ลูกค้า A ได้กิน -> ลูกค้า B สั่งไข่เจียว -> พ่อครัวทำไข่เจียว
    • ปัญหา: ถ้าลูกค้า A สั่งเมนูที่ใช้เวลาทำนานมาก (เช่น ต้มซุป 1 ชั่วโมง) ลูกค้า B จะต้องรอจนกว่าลูกค้า A จะได้ของ ถึงจะเริ่มสั่งได้ ร้านจะค้าง (Blocking)
  • Asynchronous (แบบไม่รอ): ลูกค้า A สั่งซุป -> พ่อครัวรับออเดอร์แล้วตั้งหม้อไว้ (ส่งงานไปทำเบื้องหลัง) -> หันมารับออเดอร์ลูกค้า B ต่อทันที -> พอซุปเสร็จค่อยเสิร์ฟลูกค้า A

ในโปรแกรมจริง งานที่กินเวลานานคือการดึงข้อมูลจาก Server, การอ่านไฟล์, หรือการตั้งเวลา (Timer) ถ้าเราทำแบบ Synchronous หน้าเว็บจะค้าง กดอะไรไม่ได้เลย เราจึงต้องใช้วิธี Asynchronous

ยุคที่ 1: Callback Hell (นรกของคนรอ)

ในสมัยก่อน เราใช้ Callback function เพื่อบอกว่า "ถ้างานเสร็จแล้ว ให้ทำฟังก์ชันนี้นะ"

ปัญหา

เมื่อมีการทำงานต่อกันหลายๆ ขั้นตอน โค้ดจะซ้อนกันลึกเข้าไปเรื่อยๆ จนเป็นรูปทรงพีระมิด หรือที่เรียกว่า Callback Hell ซึ่งอ่านยากและแก้ไขยากมาก

ตัวอย่าง: การโหลดข้อมูลผู้ใช้ -> โหลดโพสต์ของผู้ใช้ -> โหลดคอมเมนต์ล่าสุด

function getUser(id, callback) {
  setTimeout(() => {
   console.log("ได้ข้อมูลผู้ใช้แล้ว");
   callback({ id: id, name: "Somchai" });
  }, 1000);
}

function getPosts(user, callback) {
  setTimeout(() => {
    console.log("ได้โพสต์ของ " + user.name);
    callback(["Post 1", "Post 2"]);
  }, 1000);
}

function getComments(post, callback) {
  setTimeout(() => {
   console.log("ได้คอมเมนต์ของ " + post);
   callback(["Cool!", "Nice!"]);
  }, 1000);
}

// การเรียกใช้แบบ Callback Hell
getUser(1, function(user) {
  getPosts(user, function(posts) {
    getComments(posts[0], function(comments) {
      console.log("จบการทำงาน: " + comments);
      // ถ้ามี error ต้องดักจับในทุกชั้นเอง
    });
  });
});

ยุคที่ 2: Promise (คำสัญญา)

เพื่อแก้ปัญหา Callback Hell จึงเกิด Promise ขึ้นมาใน ES6 (2015)

Promise คือวัตถุที่แทนค่าที่ "อาจจะมีในอนาคต" (สำเร็จ หรือ ล้มเหลว) ช่วยให้เราเขียนโค้ดเป็นลำดับลงมา (Chain) ด้วย `.then()` ได้ ทำให้โค้ดแบนราบลง

ตัวอย่าง: แปลงฟังก์ชันข้างบนให้คืนค่าเป็น Promise

// สมมติว่าฟังก์ชันเหล่านี้คืนค่าเป็น Promise แล้ว
getUser(1)
  .then(user => {
    return getPosts(user); // ส่งต่อให้ .then ถัดไป
  })
  .then(posts => {
    return getComments(posts[0]);
  })
  .then(comments => {
    console.log("จบการทำงาน: " + comments);
  })
  .catch(error => {
    // ดักจับ error ได้ในจุดเดียว
    console.error(error);
  });

ถึงแม้จะดีขึ้น แต่ก็ยังมีวงเล็บและ `.then()` เยอะอยู่ดี ถ้าลอจิกซับซ้อน

ยุคปัจจุบัน: Async / Await

มาถึง keyword ของเรา Async/Await (ES2017) มันคือ "น้ำตาลเคลือบ" (Syntactic Sugar) ของ Promise ที่ช่วยให้เราเขียนโค้ด Asynchronous ให้ดูเหมือนโค้ด Synchronous ปกติ!

  • async: ใส่หน้า function เพื่อบอกว่าฟังก์ชันนี้จะทำงานแบบ Async และจะคืนค่าเป็น Promise เสมอ
  • await: ใส่หน้า Promise เพื่อสั่งให้ "หยุดรอ" บรรทัดนี้จนกว่าจะได้ผลลัพธ์ (โดยไม่บล็อกการทำงานส่วนอื่นของโปรแกรมหลัก)

ตัวอย่าง: การทำงานเดียวกันที่เขียนง่ายที่สุด

async function showUserComments() {
  try {
    // โค้ดอ่านง่ายเหมือนบรรทัดต่อบรรทัด
    const user = await getUser(1);
    const posts = await getPosts(user);
    const comments = await getComments(posts[0]);

    console.log("จบการทำงาน: " + comments);
    
  } catch (error) {
    // ใช้ try...catch แบบปกติในการดัก error
    console.error("เกิดข้อผิดพลาด:", error);
  }
}

showUserComments();

ตารางเปรียบเทียบ

คุณสมบัติ Callback Promise (.then) Async / Await
ความอ่านง่าย ยาก (ซ้อนลึก) ปานกลาง (Chain ยาว) ง่ายที่สุด (เหมือนโค้ดปกติ)
การจัดการ Error ต้องเช็คทุกชั้น ใช้ .catch() ใช้ try...catch
ลำดับการทำงาน ดูยากเมื่อซับซ้อน ชัดเจนขึ้น เป็นธรรมชาติ
Browser Support ทุกรุ่น เกือบทุกรุ่น Browser สมัยใหม่ (Modern)

สรุป

  • JavaScript ทำงานแบบ Single-Threaded
  • หลีกเลี่ยง Callback Hell เพราะดูแลรักษายาก
  • Promise ช่วยจัดระเบียบการรอข้อมูล
  • Async/Await คือวิธีที่ดีที่สุดในปัจจุบัน ทำให้โค้ดอ่านง่าย สะอาด และจัดการ Error ได้ด้วย `try...catch` ที่คุ้นเคย

ถ้าเริ่มโปรเจกต์ใหม่ หรือเรียนรู้ตอนนี้ ให้โฟกัสที่การใช้ Async/Await เป็นหลัก

ตัวอย่างโปรแกรมสำหรับทดลอง

ส่วนนี้ไม่ได้เขียนด้วย Gemini แต่มาจาก js/async

Code เริ่มต้น

function doSomething(msg) {
  let t = Math.random()*1500;
  setTimeout(() => {
    console.log(msg + "  " + t);
  }, t);
}

console.log("----started----");
doSomething("A         ");
doSomething("  B       ");
doSomething("    C     ");
doSomething("      D   ");
doSomething("        E ");      
console.log("---------------");

Callback

function doSomething(msg, callback) {
  let t = Math.random()*1500;
  setTimeout(() => {
    console.log(msg + "  " + t);
    if (callback !== undefined) callback();
  }, t);
}

console.log("----started----");
doSomething("A         ", () => {
  doSomething("  B       ", () => {
    doSomething("    C     ", () => {
      doSomething("      D   ", () => {
        doSomething("        E ");      
      });    
    });  
  });
});
console.log("---------------");

Promise

function doSomething(msg) {
  return new Promise((resolve,reject) => {
    let t = Math.random()*1500;
    setTimeout(() => {
      console.log(msg + "  " + t);
      resolve();
    }, t);
  });  
}

console.log("----started----");
doSomething("A         ")
  .then(() => {return doSomething("  B       ")})
  .then(() => {return doSomething("    C     ")})
  .then(() => {return doSomething("       D  ")})
  .then(() => {return doSomething("         E")});
console.log("---------------");

Async / await

function doSomething(msg) {
  return new Promise((resolve,reject) => {
    let t = Math.random()*1500;
    setTimeout(() => {
      console.log(msg + "  " + t);
      resolve();
    }, t);
  });  
}

async function main() {
  await doSomething("A         ");
  await doSomething("  B       ");
  await doSomething("    C     ");
  await doSomething("       D  ");
  await doSomething("         E");
  return 10;
}

function main2alternative() {
  return (main()
            .then((x) => { return doSomething(" Z " + x); }));
}

async function main2() {
  let x = await main();
  await doSomething(" Z " + x);
}

async function main3() {
  await main2();
  await doSomething("---------");
}

console.log("----started----");
main3();
console.log("---------------");