17: Asynchronous JavaScript

จัดการการทำงานแบบ "ไม่รอคิว" หัวใจสำคัญของการต่อ API และ Database

1. Concept: Sync vs Async

เปรียบเทียบการทำงานของ JavaScript กับ "ร้านอาหารตามสั่ง"

Synchronous (แบบรอคิว)

ทำงานทีละบรรทัดจากบนลงล่าง ถ้าบรรทัดไหนช้า บรรทัดต่อไปต้องรอ (เหมือนพ่อครัวทำอาหารเสร็จทีละโต๊ะ ถึงจะรับออเดอร์โต๊ะต่อไปได้)

JS
console.log("1. รับออเดอร์โต๊ะ 1");
// สมมติว่าตรงนี้ใช้เวลาทำอาหาร 5 วินาที (หน้าเว็บจะค้างไปเลย)
console.log("2. เสิร์ฟโต๊ะ 1");
console.log("3. รับออเดอร์โต๊ะ 2"); // ต้องรอโต๊ะ 1 เสร็จก่อน
Asynchronous (แบบไม่รอคิว)

สั่งงานทิ้งไว้ แล้วข้ามไปทำบรรทัดอื่นก่อน เสร็จเมื่อไหร่ค่อยกลับมาบอก (เหมือนพ่อครัวรับออเดอร์ไว้ แล้วไปผัดจานอื่นพลาง ๆ จานไหนเสร็จก่อนเสิร์ฟก่อน)

JS
console.log("1. รับออเดอร์โต๊ะ 1 (ส่งเข้าครัว)");

// จำลองการใช้เวลา 2 วินาที (ไม่บล็อกการทำงาน)
setTimeout(() => {
    console.log("3. อาหารโต๊ะ 1 เสร็จแล้ว นำไปเสิร์ฟ");
}, 2000);

console.log("2. รับออเดอร์โต๊ะ 2 ต่อทันที"); 
// ลำดับที่ออก: 1 -> 2 -> 3(รอ 2 วิ)

2. ยุคมืด: Callback Hell

สมัยก่อนตอนที่ยังไม่มี Promise การทำงานที่ต้องรอคิวต่อ ๆ กัน (เช่น ล็อกอิน -> ดึงโปรไฟล์ -> ดึงออเดอร์) ต้องใช้ฟังก์ชันซ้อนฟังก์ชัน และต้องคอยเขียน if (error) ดักข้อผิดพลาดเอาไว้ "ทุก ๆ ชั้น" ทำให้โค้ดลึกเป็นรูปสามเหลี่ยม (Pyramid of Doom)

1. สร้างฟังก์ชันจำลอง (API Mocks)

คัดลอกโค้ดนี้ไปใช้ เพื่อสร้างระบบหลังบ้านจำลอง (ใช้รูปแบบ Error-First Callback)

Setup: Mock Functions
// 1. ฟังก์ชัน Login
function loginUser(user, pass, callback) {
    setTimeout(() => {
        if (pass === "1234") callback(null, { id: 99, name: user });
        else callback("รหัสผ่านไม่ถูกต้อง!", null);
    }, 500);
}

// 2. ฟังก์ชันดึง Profile
function getProfile(userId, callback) {
    setTimeout(() => {
        if (userId === 99) callback(null, { id: 99, email: "somchai@mail.com" });
        else callback("ไม่พบผู้ใช้งานในระบบ", null);
    }, 500);
}

// 3. ฟังก์ชันดึง Order
function getOrders(userId, callback) {
    setTimeout(() => {
        callback(null, [{ id: "ORD-001", total: 500 }]); 
    }, 500);
}

// 4. ฟังก์ชันดึงสถานะการจ่ายเงิน
function getPaymentStatus(orderId, callback) {
    setTimeout(() => {
        if (orderId === "ORD-001") callback(null, "PAID (จ่ายแล้ว)");
        else callback("ไม่พบข้อมูลออเดอร์", null);
    }, 500);
}
2. ทดสอบ: เคสสำเร็จ (ใส่รหัสถูก)

โค้ดลึก 4 ชั้น และต้องดัก err 4 รอบ!

JS
loginUser("somchai", "1234", (err1, user) => {
    if (err1) return console.error("Error 1:", err1);
    
    getProfile(user.id, (err2, profile) => {
        if (err2) return console.error("Error 2:", err2);
        
        getOrders(profile.id, (err3, orders) => {
            if (err3) return console.error("Error 3:", err3);
            
            getPaymentStatus(orders[0].id, (err4, status) => {
                if (err4) return console.error("Error 4:", err4);
                
                console.log("เข้าสู่ระบบสำเร็จ สถานะ:", status); 
            });
        });
    });
});
3. ทดสอบ: เคสพัง (ใส่รหัสผิด)

ถ้าใส่ wrongpass มันจะเด้ง Error ตั้งแต่ชั้นแรก

JS
loginUser("somchai", "wrongpass", (err1, user) => {
    // โดนดักตรงนี้ทันที!
    if (err1) return console.error("Error ชั้นที่ 1:", err1);
    
    // โค้ดด้านล่างนี้จะไม่ถูกรันเลย
    getProfile(user.id, (err2, profile) => {
        if (err2) return console.error("Error ชั้นที่ 2:", err2);
        // ...
    });
});

3. พระเอกขี่ม้าขาว: Promise

เพื่อแก้ปัญหา Callback Hell JavaScript จึงสร้าง Promise ขึ้นมา มันคือ Object ที่เปลี่ยนจากการส่งฟังก์ชันซ้อนกันไปเรื่อย ๆ มาเป็นการ "ต่อคิว" (Chaining) แทน

  • .then((data) => { ... }) : ทำงานเมื่อสำเร็จ (แล้วส่งค่าไปคิวต่อไป)
  • .catch((error) => { ... }) : ดักจับ Error รวบยอดแบบครั้งเดียวตอนจบ (ไม่ต้องเขียน if (err) ดักทุกชั้นแล้ว)
1. อัปเกรดหลังบ้าน: เปลี่ยนฟังก์ชันให้เป็น Promise

ก่อนจะเรียกใช้แบบใหม่ ต้องจำลองฟังก์ชันทั้ง 4 ตัวก่อนหน้า ให้คืนค่าเป็น Promise ก่อน

Mock Functions (Promise version)
// หลังบ้านยุคใหม่ ต้องเปลี่ยนฟังก์ชันให้ Return Promise (สมมติว่าเซิร์ฟเวอร์อัปเกรดให้แล้ว)
const loginUserAsync = (user, pass) => new Promise((resolve, reject) => {
    setTimeout(() => pass === "1234" ? resolve({ id: 99, name: user }) : reject("รหัสผ่านไม่ถูกต้อง!"), 500);
});
const getProfileAsync = (id) => new Promise((resolve, reject) => {
    setTimeout(() => id === 99 ? resolve({ id: 99, email: "s@mail.com" }) : reject("ไม่พบผู้ใช้"), 500);
});
const getOrdersAsync = (id) => new Promise(resolve => setTimeout(() => resolve([{ id: "ORD-001" }]), 500));
const getPaymentStatusAsync = (orderId) => new Promise((resolve, reject) => {
    setTimeout(() => orderId === "ORD-001" ? resolve("PAID (จ่ายแล้ว)") : reject("ไม่พบออเดอร์"), 500);
});
2. นำฟังก์ชันแบบ Promise มาเรียกใช้งาน

สังเกตว่าโค้ดจะไม่ลึกเป็นรูปสามเหลี่ยมแล้ว แต่จะเรียงต่อกันเป็นเส้นตรงลงมา (Chain)

Promise Chaining (แบบมาตรฐาน)
loginUserAsync("somchai", "1234")
    .then((user) => {
        // ได้ user มา ส่งไปหา profile ต่อ
        return getProfileAsync(user.id); 
    })
    .then((profile) => {
        // ได้ profile มา ส่งไปหา orders ต่อ
        return getOrdersAsync(profile.id);
    })
    .then((orders) => {
        // ได้ orders มา ส่งไปหาสถานะจ่ายเงินต่อ
        return getPaymentStatusAsync(orders[0].id);
    })
    .then((status) => {
        // จบกระบวนการ นำข้อมูลไปใช้
        console.log("เข้าสู่ระบบสำเร็จ สถานะ:", status); 
    })
    .catch((err) => {
        // ถ้าพังที่ชั้นไหนก็ตาม (เช่น รหัสผิด หรือหาออเดอร์ไม่เจอ) 
        // มันจะกระโดดข้ามมาเข้ากล่อง catch นี้ทันที!
        console.error("เกิดข้อผิดพลาด:", err);
    });
Tip: เขียนแบบย่อ (Shorthand)

ด้วยพลังของ Arrow Function บรรทัดเดียว (ที่ละคำว่า return ได้) โปรแกรมเมอร์ยุค ES6 นิยมเขียน Promise ให้สั้นได้แบบนี้เลย

JS
loginUserAsync("somchai", "1234")
    .then(user => getProfileAsync(user.id))
    .then(profile => getOrdersAsync(profile.id))
    .then(orders => getPaymentStatusAsync(orders[0].id))
    .then(status => console.log("เข้าสู่ระบบสำเร็จ สถานะ:", status))
    .catch(err => console.error("เกิดข้อผิดพลาด:", err));

4. ท่าไม้ตาย: Async / Await

นิยมใช้ใน Node.js

เพื่อหนีจาก Promise Chain ยาว ๆ ปัจจุบันมักจะใช้ async / await ในการ "เขียนโค้ดที่ต้องรอ ให้หน้าตาเหมือนโค้ดปกติ"

กฎทอง 2 ข้อ:
  1. คำสั่งไหนต้องรอ ให้ใส่ await ไว้ข้างหน้า และฟังก์ชันที่ครอบมันอยู่ ต้องมี คำว่า async เสมอ
  2. ใช้ try { ... } catch (err) { ... } ครอบโค้ดไว้เพื่อ ดัก Error รวบยอดในกล่องเดียว
เปรียบเทียบ: นำโค้ด Promise จากหัวข้อที่แล้วมาเขียนใหม่ด้วย Async/Await

จะใช้ฟังก์ชัน loginUserAsync และผองเพื่อน จากหัวข้อที่ 3 มาเรียกใช้ด้วยท่าใหม่ล่าสุด

✅ ทดสอบ: เคสสำเร็จ (รหัสถูก)
JS
async function runSuccess() {
    try {
        // โค้ดเรียงตัวสวยงาม ไม่มี Callback ลึกๆ อีกต่อไป!
        let user = await loginUserAsync("somchai", "1234");
        let profile = await getProfileAsync(user.id);
        let orders = await getOrdersAsync(profile.id);
        let status = await getPaymentStatusAsync(orders[0].id);
        
        console.log("เข้าสู่ระบบสำเร็จ สถานะ:", status);
    } catch (err) {
        // ถ้าบรรทัดไหนใน try พัง มันกระโดดมานี่ทันที
        console.error("เกิดข้อผิดพลาด:", err);
    }
}

runSuccess();
❌ ทดสอบ: เคสพัง (รหัสผิด)
JS
async function runFail() {
    try {
        // ใส่รหัสผิด พังตั้งแต่บรรทัดนี้!
        let user = await loginUserAsync("somchai", "wrong");
        
        // 4 บรรทัดนี้จะไม่ถูกทำงานเลย
        let profile = await getProfileAsync(user.id);
        let orders = await getOrdersAsync(profile.id);
        let status = await getPaymentStatusAsync(orders[0].id);

        console.log("เข้าสู่ระบบสำเร็จ สถานะ:", status);
    } catch (err) {
        // ดัก Error รวบยอด กล่องเดียวจบ!
        console.error("เกิดข้อผิดพลาด:", err); 
        // แสดงผล: "เกิดข้อผิดพลาด: รหัสผ่านไม่ถูกต้อง!"
    }
}

runFail();
Tip: การเขียน Async ร่วมกับ Arrow Function

ในการทำงานจริง (โดยเฉพาะ React) นิยมใช้ Arrow Function คู่กับ async/await มากกว่าฟังก์ชันแบบปกติ โดยกฎง่าย ๆ คือให้แปะคำว่า async ไว้ หน้าวงเล็บ () เสมอ

1. แบบสร้างเป็นตัวแปร (Variable Function)
// แบบเดิม: async function fetchUser() { ... }

// แบบ Arrow Function:
const fetchUser = async () => {
    try {
        let user = await loginUserAsync("somchai", "1234");
        console.log("ได้ข้อมูล:", user);
    } catch (err) {
        console.error("พังซะแล้ว:", err);
    }
};

fetchUser(); // เรียกใช้งานตามปกติ
2. ใช้ใน Event Listener (เจอบ่อยมาก)
const btn = document.querySelector("#myBtn");

// แปะ async ไว้หน้า () ของ Arrow Function ได้เลย
btn.addEventListener("click", async () => {
    // สามารถใช้ await ดึงข้อมูลตอนโดนคลิกได้ทันที
    console.log("กำลังโหลด...");
    let data = await getProfileAsync(99); 
    console.log("เสร็จแล้ว:", data);
});

Workshop: Data Fetching

จำลองการดึงข้อมูลจาก Server ที่ต้องใช้เวลา 2 วินาที

คลิกปุ่มด้านบนเพื่อเริ่ม..
Logic (Async/Await)
// ฟังก์ชันจำลองการโหลดข้อมูล (คืนค่าเป็น Promise)
function mockAPI() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve("โหลดข้อมูลเสร็จสมบูรณ์");
        }, 2000); // ดีเลย์ 2 วินาที
    });
}
Practice Mission: ระบบค้นหาพนักงาน
สถานการณ์:

คุณกำลังทำหน้าระบบ HR เมื่อกรอกรหัสพนักงานแล้วกดค้นหา ระบบจะต้องรอข้อมูลจาก Database 1.5 วินาที ถ้าเจอให้แสดงข้อมูล (รหัส 101, 102) แต่ถ้าไม่เจอให้แสดงข้อความแจ้งเตือนสีแดง (ใช้ try...catch ดักจับ)

ระบบค้นหาพนักงาน
Requirements:
  1. คัดลอกฟังก์ชัน findEmployeeInDB(id) ที่คืนค่าเป็น Promise ไปใช้แทนการเรียกข้อมูลจากเซิร์ฟเวอร์
  2. JS
    // ฟังก์ชันจำลองผลลัพธ์จากเซิร์ฟเวอร์
    function findEmployeeInDB(id) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                if (id === "101") resolve({ id: 101, name: "Tony Stark", dept: "Engineering" });
                else if (id === "102") resolve({ id: 102, name: "Natasha Romanoff", dept: "Human Resources" });
                else reject("ไม่พบพนักงานรหัสนี้ในระบบ!"); // กรณี Error
            }, 1500);
        });
    }
  3. ใช้ addEventListener ดักจับการส่งฟอร์ม (อย่าลืม preventDefault)
  4. สร้างฟังก์ชัน async
  5. ใน try { ... } ให้แสดง Loading -> await ข้อมูล -> ซ่อน Loading -> แสดง HTML ข้อมูลพนักงาน
  6. ใน catch (err) { ... } ให้ซ่อน Loading -> แสดงข้อความ err เป็นสีแดง