17: Asynchronous JavaScript
จัดการการทำงานแบบ "ไม่รอคิว" หัวใจสำคัญของการต่อ API และ Database
1. Concept: Sync vs Async
เปรียบเทียบการทำงานของ JavaScript กับ "ร้านอาหารตามสั่ง"
Synchronous (แบบรอคิว)
ทำงานทีละบรรทัดจากบนลงล่าง ถ้าบรรทัดไหนช้า บรรทัดต่อไปต้องรอ (เหมือนพ่อครัวทำอาหารเสร็จทีละโต๊ะ ถึงจะรับออเดอร์โต๊ะต่อไปได้)
console.log("1. รับออเดอร์โต๊ะ 1");
// สมมติว่าตรงนี้ใช้เวลาทำอาหาร 5 วินาที (หน้าเว็บจะค้างไปเลย)
console.log("2. เสิร์ฟโต๊ะ 1");
console.log("3. รับออเดอร์โต๊ะ 2"); // ต้องรอโต๊ะ 1 เสร็จก่อน
Asynchronous (แบบไม่รอคิว)
สั่งงานทิ้งไว้ แล้วข้ามไปทำบรรทัดอื่นก่อน เสร็จเมื่อไหร่ค่อยกลับมาบอก (เหมือนพ่อครัวรับออเดอร์ไว้ แล้วไปผัดจานอื่นพลาง ๆ จานไหนเสร็จก่อนเสิร์ฟก่อน)
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)
// 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 รอบ!
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
ตั้งแต่ชั้นแรก
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 ก่อน
// หลังบ้านยุคใหม่ ต้องเปลี่ยนฟังก์ชันให้ 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)
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
ให้สั้นได้แบบนี้เลย
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 ข้อ:
- คำสั่งไหนต้องรอ ให้ใส่
awaitไว้ข้างหน้า และฟังก์ชันที่ครอบมันอยู่ ต้องมี คำว่าasyncเสมอ - ใช้
try { ... } catch (err) { ... }ครอบโค้ดไว้เพื่อ ดัก Error รวบยอดในกล่องเดียว
เปรียบเทียบ: นำโค้ด Promise จากหัวข้อที่แล้วมาเขียนใหม่ด้วย Async/Await
จะใช้ฟังก์ชัน loginUserAsync และผองเพื่อน จากหัวข้อที่ 3 มาเรียกใช้ด้วยท่าใหม่ล่าสุด
✅ ทดสอบ: เคสสำเร็จ (รหัสถูก)
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();
❌ ทดสอบ: เคสพัง (รหัสผิด)
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 ไว้ หน้าวงเล็บ
() เสมอ
// แบบเดิม: async function fetchUser() { ... }
// แบบ Arrow Function:
const fetchUser = async () => {
try {
let user = await loginUserAsync("somchai", "1234");
console.log("ได้ข้อมูล:", user);
} catch (err) {
console.error("พังซะแล้ว:", err);
}
};
fetchUser(); // เรียกใช้งานตามปกติ
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 วินาที
// ฟังก์ชันจำลองการโหลดข้อมูล (คืนค่าเป็น Promise)
function mockAPI() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("โหลดข้อมูลเสร็จสมบูรณ์");
}, 2000); // ดีเลย์ 2 วินาที
});
}
Practice Mission: ระบบค้นหาพนักงาน
สถานการณ์:
คุณกำลังทำหน้าระบบ HR เมื่อกรอกรหัสพนักงานแล้วกดค้นหา ระบบจะต้องรอข้อมูลจาก Database 1.5 วินาที ถ้าเจอให้แสดงข้อมูล (รหัส 101, 102) แต่ถ้าไม่เจอให้แสดงข้อความแจ้งเตือนสีแดง (ใช้ try...catch ดักจับ)
Requirements:
- คัดลอกฟังก์ชัน
findEmployeeInDB(id)ที่คืนค่าเป็น Promise ไปใช้แทนการเรียกข้อมูลจากเซิร์ฟเวอร์ - ใช้
addEventListenerดักจับการส่งฟอร์ม (อย่าลืม preventDefault) - สร้างฟังก์ชัน
async - ใน
try { ... }ให้แสดง Loading ->awaitข้อมูล -> ซ่อน Loading -> แสดง HTML ข้อมูลพนักงาน - ใน
catch (err) { ... }ให้ซ่อน Loading -> แสดงข้อความerrเป็นสีแดง
// ฟังก์ชันจำลองผลลัพธ์จากเซิร์ฟเวอร์
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);
});
}