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)

สรุป

1. JavaScript ทำงานแบบ **Single-Threaded** 2. หลีกเลี่ยง **Callback Hell** เพราะดูแลรักษายาก 3. **Promise** ช่วยจัดระเบียบการรอข้อมูล 4. **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("---------------");