🔵 Node.js

เจาะลึก Node.js Asynchronous Programming: จาก Callback สู่ async/await

9 นาที 18 views บันทึกเป็น PDF
เจาะลึก Node.js Asynchronous Programming: จาก Callback สู่ async/await

มือใหม่หัดเขียน Node.js ต้องรู้! ทำความเข้าใจการจัดการงานแบบ Asynchronous เพื่อแก้ปัญหา Callback Hell ให้โค้ดสะอาด อ่านง่าย และทำงานได้มีประสิทธิภาพ

ทำไมโปรแกรมเมอร์ต้องรู้จัก Asynchronous Programming

เวลาเราเขียนโปรแกรมทั่วไป คอมพิวเตอร์จะทำตามคำสั่งจากบรรทัดบนลงล่างทีละขั้นตอน ซึ่งเราเรียกการทำงานแบบนี้ว่า Synchronous (การทำงานแบบตามลำดับที่ต้องรอให้งานเก่าเสร็จก่อนถึงจะเริ่มงานใหม่ได้) เหมือนกับการต่อคิวซื้อกาแฟที่ทุกคนต้องรอให้คนข้างหน้าได้รับเครื่องดื่มก่อนถึงจะสั่งได้ ซึ่งถ้าคิวแรกสั่งเมนูที่ทำยาก พนักงานก็จะยืนทำเมนูนั้นคนเดียวจนกว่าจะเสร็จ ทำให้คิวข้างหลังเสียเวลาไปเปล่าๆ

แต่ในโลกของ Node.js (สภาพแวดล้อมที่ช่วยให้รันภาษา JavaScript บนเครื่องคอมพิวเตอร์ได้) เรามักต้องทำงานที่ใช้เวลานาน เช่น การดึงข้อมูลจากฐานข้อมูล หรือการอ่านไฟล์ขนาดใหญ่ ถ้าเราใช้การทำงานแบบตามลำดับ โปรแกรมจะค้างจนกว่างานนั้นจะเสร็จ ซึ่งส่งผลเสียต่อ User Experience (ประสบการณ์ที่ผู้ใช้งานได้รับ) อย่างมาก

เราจึงต้องใช้ Asynchronous Programming (การจัดการงานแบบไม่รอให้งานเดิมเสร็จก่อน) เพื่อให้โปรแกรมทำงานอื่นต่อไปได้ในระหว่างที่รอกระบวนการที่ใช้เวลานาน นี่คือทักษะสำคัญที่โปรแกรมเมอร์ทุกคนต้องมี เพราะมันช่วยให้แอปพลิเคชันของเราตอบสนองได้รวดเร็วและรองรับผู้ใช้งานได้พร้อมกันหลายคนครับ

รู้จักกับ Callback Functions

Callback Functions (ฟังก์ชันที่ถูกส่งเข้าไปเป็นอาร์กิวเมนต์เพื่อให้ทำงานหลังจากงานหลักเสร็จสิ้น) คือวิธีพื้นฐานที่สุดในการจัดการงานที่ไม่รู้ว่าจะเสร็จเมื่อไหร่ ลองนึกภาพว่าคุณสั่งอาหารแล้วพนักงานให้บัตรคิวมาแทนที่จะยืนรอหน้าเคาน์เตอร์ เมื่ออาหารเสร็จพนักงานจะเรียกเลขคิวของคุณ นั่นแหละคือหลักการของ Callback

ในภาษา JavaScript เราสามารถส่งฟังก์ชันเข้าไปในฟังก์ชันอื่นได้ เพื่อบอกว่า "เมื่อทำงานนี้เสร็จแล้ว ให้เรียกฟังก์ชันนี้ต่อด้วยนะ" วิธีนี้ช่วยให้เราไม่ต้องหยุดโปรแกรมเพื่อรอผลลัพธ์ แต่เราต้องระวังให้ดี เพราะถ้าเขียนซ้อนกันเยอะเกินไป โค้ดจะอ่านยากมากจนกลายเป็นปัญหาที่เรียกว่า Callback Hell (สภาวะที่โค้ดซ้อนกันเป็นชั้นๆ จนดูเหมือนพีระมิด)

ลองดูตัวอย่างการใช้ Callback ในการอ่านไฟล์กันครับ

const fs = require('fs'); // ดึงโมดูลสำหรับจัดการไฟล์

fs.readFile('data.txt', 'utf8', (err, data) => {
  if (err) {
    console.log('เกิดข้อผิดพลาด:', err);
    return;
  }
  console.log('ข้อมูลที่อ่านได้:', data);
});

ในโค้ดนี้ fs.readFile คือฟังก์ชันที่สั่งอ่านไฟล์ โดยเราส่งฟังก์ชันลูกศร (err, data) => { ... } เข้าไปเป็นพารามิเตอร์ตัวสุดท้าย บรรทัดแรกคือการนำเข้า fs (File System) เพื่อใช้คำสั่งอ่านไฟล์ บรรทัดที่สองคือการเรียกคำสั่งโดยระบุชื่อไฟล์และรูปแบบตัวอักษร ส่วนฟังก์ชันข้างในจะทำงานทันทีที่อ่านไฟล์เสร็จ โดยมี err ไว้เก็บกรณีเกิดข้อผิดพลาด และ data ไว้เก็บเนื้อหาในไฟล์

ผลลัพธ์ที่ควรเห็น: หากไฟล์ data.txt มีข้อความว่า "Hello World" หน้าจอจะแสดงผลว่า ข้อมูลที่อ่านได้: Hello World

ทำความเข้าใจปัญหา Callback Hell

Callback Hell (ปัญหาโค้ดซ้อนกันลึกจนอ่านไม่ออก) เกิดขึ้นเมื่อเราต้องทำงานหลายอย่างที่ต้องทำต่อกันเป็นทอดๆ เช่น ต้องอ่านไฟล์ก่อน แล้วค่อยส่งข้อมูลนั้นไปเขียนไฟล์ใหม่ แล้วค่อยส่งไปบันทึกในฐานข้อมูล ถ้าใช้ Callback แบบเดิม เราจะเขียนฟังก์ชันซ้อนกันไปเรื่อยๆ จนโค้ดเอียงไปทางขวาเหมือนบันได

สถานการณ์นี้ทำให้การจัดการข้อผิดพลาดทำได้ยากมาก เพราะเราต้องเช็ค if (err) ในทุกๆ ชั้นที่ซ้อนกันอยู่ ถ้าลืมเช็คตรงไหนไปสักจุด โปรแกรมอาจจะพังโดยที่เราไม่รู้สาเหตุเลยว่าเกิดอะไรขึ้นที่บรรทัดไหน การแก้ไขโค้ดที่ซ้อนกันแบบนี้ในอนาคตจึงเป็นเรื่องที่เหนื่อยมากสำหรับโปรแกรมเมอร์

ให้ลองจินตนาการว่าคุณต้องทำภารกิจ 3 อย่างที่ต้องรอผลลัพธ์จากกันและกัน โค้ดที่ไม่มีการจัดการที่ดีจะดูยุ่งเหยิงและไม่มีระเบียบแบบนี้ครับ

// ตัวอย่างแบบที่ควรหลีกเลี่ยง (Callback Hell)
doStep1((err, result1) => {
  doStep2(result1, (err, result2) => {
    doStep3(result2, (err, result3) => {
      console.log('ผลลัพธ์สุดท้าย:', result3);
    });
  });
});

บรรทัดแรกเริ่มทำงานขั้นตอนที่ 1 เมื่อเสร็จจะเรียก doStep2 ต่อไปเรื่อยๆ จนถึง doStep3 สังเกตว่าวงเล็บและปีกกาจะซ้อนกันลึกเข้าไปเรื่อยๆ ทำให้โค้ดอ่านยากและดูแลรักษายากมาก นี่คือสัญญาณเตือนว่าคุณต้องเริ่มหาวิธีอื่นมาแทนที่ Callback แล้วครับ

ผลลัพธ์ที่ควรเห็น: โปรแกรมจะพิมพ์ข้อความ ผลลัพธ์สุดท้าย: [ค่าที่ได้] ออกมา แต่ถ้ามีขั้นตอนที่ 4 หรือ 5 โค้ดจะเริ่มดูไม่ได้เลย

แก้ปัญหาด้วย Promises

Promises (วัตถุที่ใช้แทนค่าที่อาจจะยังไม่มีในตอนนี้ แต่จะมีในอนาคต) ถูกสร้างมาเพื่อแก้ปัญหา Callback Hell โดยการเปลี่ยนโครงสร้างโค้ดแบบซ้อนกัน ให้กลายเป็นการเขียนแบบเรียงแถวลงมาแทน Promise จะมีสถานะอยู่ 3 อย่างคือ Pending (กำลังรอ), Fulfilled (สำเร็จ), และ Rejected (ล้มเหลว)

เราใช้ .then() เพื่อระบุว่าถ้างานสำเร็จให้ทำอะไรต่อ และใช้ .catch() เพื่อดักจับข้อผิดพลาดที่เกิดขึ้นในทุกขั้นตอน นี่ทำให้โค้ดดูสะอาดขึ้นและจัดการ Error Handling (การจัดการข้อผิดพลาด) ได้รวมศูนย์มากขึ้น ไม่ต้องคอยเช็ค err ในทุกๆ บรรทัดอีกต่อไป

ลองดูตัวอย่างการใช้ Promise แทนที่โค้ดแบบเก่ากันครับ

doStep1()
  .then(result1 => doStep2(result1))
  .then(result2 => doStep3(result2))
  .then(result3 => console.log('ผลลัพธ์:', result3))
  .catch(err => console.error('พบข้อผิดพลาด:', err));

ในโค้ดนี้ doStep1() จะส่งคืนค่า Promise ออกมา ทำให้เราสามารถใช้ .then() มาต่อท้ายได้แบบลูกโซ่ (Chain) ถ้าขั้นตอนใดขั้นตอนหนึ่งเกิดข้อผิดพลาด Promise จะข้ามการทำงานใน .then() ไปที่ .catch() ทันที ทำให้เราเขียนโค้ดที่จัดการความผิดพลาดไว้ที่เดียวตอนท้ายได้เลย

ผลลัพธ์ที่ควรเห็น: หน้าจอจะแสดง ผลลัพธ์: [ค่าสุดท้าย] หากทุกอย่างสำเร็จ หรือแสดงข้อความใน catch หากมีขั้นตอนใดขั้นตอนหนึ่งทำงานล้มเหลว

ก้าวสู่ความเชี่ยวชาญด้วย async/await

async/await (รูปแบบการเขียนโค้ดที่ทำให้ Asynchronous ดูเหมือนโค้ดทั่วไปที่ทำงานตามลำดับ) เป็นวิธีที่ได้รับความนิยมที่สุดในปัจจุบัน มันถูกสร้างขึ้นมาครอบบน Promise อีกทีเพื่อให้เราเขียนโค้ดได้อ่านง่ายเหมือนภาษาโปรแกรมแบบปกติ โดยไม่ต้องมี .then() มาคั่นกลางให้ลายตา

คำว่า async จะถูกใส่ไว้หน้าฟังก์ชันเพื่อบอกว่าฟังก์ชันนี้จะมีการทำงานแบบรอผลลัพธ์ ส่วน await จะใช้หน้าคำสั่งที่ต้องรอผลลัพธ์จาก Promise เมื่อโปรแกรมอ่านถึงบรรทัดที่มี await มันจะหยุดรอผลลัพธ์ตรงนั้นก่อนจะไปบรรทัดถัดไป แต่การหยุดนี้จะไม่ทำให้ทั้งแอปพลิเคชันค้างครับ

ลองเปลี่ยนโค้ดจาก Promise มาเป็น async/await ดูครับ

async function runTasks() {
  try {
    const r1 = await doStep1();
    const r2 = await doStep2(r1);
    const r3 = await doStep3(r2);
    console.log('ผลลัพธ์:', r3);
  } catch (err) {
    console.error('พบข้อผิดพลาด:', err);
  }
}
runTasks();

เราต้องห่อคำสั่ง await ไว้ในบล็อก try...catch เพื่อจัดการข้อผิดพลาดแทน .catch() ของ Promise บรรทัด const r1 = await doStep1() จะรอให้งานเสร็จแล้วเก็บค่าไว้ในตัวแปร r1 ทันที วิธีนี้ทำให้โค้ดของเราดูเป็นธรรมชาติและเรียงลำดับขั้นตอนได้อย่างชัดเจนที่สุด

ผลลัพธ์ที่ควรเห็น: ผลลัพธ์จะเหมือนกับ Promise ทุกประการ แต่โค้ดจะอ่านง่ายและจัดการได้สะดวกกว่ามากในโปรเจกต์ขนาดใหญ่

สรุป: นำไปใช้จริงในการทำงาน

การเข้าใจการทำงานแบบ Asynchronous เป็นด่านสำคัญที่แยกมือใหม่กับโปรแกรมเมอร์ที่ทำงานจริงออกจากกัน ในชีวิตการทำงานจริง คุณจะต้องดึงข้อมูลจาก API (ช่องทางเชื่อมต่อระหว่างแอปฯ) หรือเชื่อมต่อกับฐานข้อมูลอยู่ตลอดเวลา การใช้ async/await จะช่วยให้คุณเขียนโค้ดที่สะอาดและลดโอกาสเกิดบั๊กได้มหาศาล

คำแนะนำสำหรับมือใหม่คือ ให้เริ่มฝึกเปลี่ยนโค้ด Callback เก่าๆ มาเป็น Promise แล้วค่อยอัปเกรดเป็น async/await ในทุกๆ ฟังก์ชันที่ต้องดึงข้อมูลภายนอก อย่าลืมว่าการทำ Error Handling ด้วย try...catch ทุกครั้งเป็นวินัยที่ดีมาก เพราะจะช่วยให้โปรแกรมของคุณไม่ปิดตัวลงทันทีเมื่อเจอข้อมูลผิดพลาด

ตัวอย่างการนำไปใช้จริง: สมมติว่าคุณกำลังสร้างแอปฯ สภาพอากาศ คุณต้องดึงข้อมูลจากเซิร์ฟเวอร์ก่อน แล้วค่อยเอาข้อมูลนั้นไปแสดงผลที่หน้าจอ ให้คุณเขียนฟังก์ชัน fetchWeather ที่ใช้ async แล้ว await ผลลัพธ์จาก API จากนั้นค่อยเรียกฟังก์ชันแสดงผล วิธีนี้จะทำให้แอปฯ ของคุณดูลื่นไหลและเป็นมืออาชีพครับ

แชร์บทความ

Facebook X LINE

บทความที่เกี่ยวข้อง

คู่มือการทำ Production Deployment: รันโปรเจกต์ด้วย PM2 และ Docker ให้เสถียรเหมือนมือโปร
Node.js

คู่มือการทำ Production Deployment: รันโปรเจกต์ด้วย PM2 และ Docker ให้เสถียรเหมือนมือโปร

เรียนรู้วิธีนำโปรเจกต์ขึ้นเซิร์ฟเวอร์จริงให้รันได้ 24 ชั่วโมงด้วย PM2 และใช้ Docker ช่วยคุมสภาพแวดล้อมให้เหมือนกันทุกเครื่อง ลดปัญหาโค้ดพังเมื่อย้ายที่

1 week ago 11 นาที
13 views
เปลี่ยนโปรเจกต์ Node.js เป็น TypeScript: วิธีตั้งค่าและใช้งานสำหรับมือใหม่
Node.js

เปลี่ยนโปรเจกต์ Node.js เป็น TypeScript: วิธีตั้งค่าและใช้งานสำหรับมือใหม่

อยากเปลี่ยนจาก Node.js มาใช้ TypeScript แต่ไม่รู้เริ่มยังไง? บทความนี้สอนตั้งค่า tsconfig.json, การรันด้วย tsx และการทำ Type Checking ให้ Express แบบเข้าใจง่าย

1 week ago 9 นาที
11 views
สอนเขียน Automated Testing ด้วย Jest และ Supertest สำหรับมือใหม่
Node.js

สอนเขียน Automated Testing ด้วย Jest และ Supertest สำหรับมือใหม่

เลิกทดสอบโค้ดด้วยมือแบบเดิม ๆ มาหัดเขียน Automated Testing ด้วย Jest และ Supertest เพื่อเช็กฟังก์ชันและ API ให้ทำงานถูกต้องแม่นยำ ลดบั๊กกวนใจ

1 week ago 9 นาที
14 views