เรียนรู้วิธีใช้ AbortController ของ JavaScript เพื่อยกเลิกการทำงานแบบ Asynchronous เช่น fetch requests, timers และอื่นๆ อย่างมีประสิทธิภาพ เพื่อให้โค้ดสะอาดและมีประสิทธิภาพมากขึ้น
JavaScript AbortController: การจัดการการยกเลิกการทำงานแบบ Asynchronous อย่างมืออาชีพ
ในการพัฒนาเว็บสมัยใหม่ การทำงานแบบอะซิงโครนัสเป็นสิ่งที่พบเห็นได้ทั่วไป การดึงข้อมูลจาก API, การตั้งเวลา และการจัดการกับการโต้ตอบของผู้ใช้ มักเกี่ยวข้องกับโค้ดที่ทำงานอย่างอิสระและอาจใช้เวลานาน อย่างไรก็ตาม มีบางสถานการณ์ที่คุณจำเป็นต้องยกเลิกการดำเนินการเหล่านี้ก่อนที่มันจะเสร็จสมบูรณ์ นี่คือจุดที่ AbortController
interface ใน JavaScript เข้ามาช่วย มันเป็นวิธีที่สะอาดและมีประสิทธิภาพในการส่งสัญญาณขอยกเลิกไปยัง DOM operations และงานอะซิงโครนัสอื่นๆ
ทำความเข้าใจถึงความจำเป็นในการยกเลิก
ก่อนที่จะลงลึกในรายละเอียดทางเทคนิค เรามาทำความเข้าใจกันก่อนว่าทำไมการยกเลิกการทำงานแบบอะซิงโครนัสจึงมีความสำคัญ ลองพิจารณาสถานการณ์ทั่วไปเหล่านี้:
- การนำทางของผู้ใช้ (User Navigation): ผู้ใช้เริ่มการค้นหา ซึ่งกระตุ้นการเรียก API หากพวกเขานำทางไปยังหน้าอื่นอย่างรวดเร็วก่อนที่การร้องขอจะเสร็จสมบูรณ์ การร้องขอเดิมก็จะไม่เกี่ยวข้องอีกต่อไปและควรถูกยกเลิกเพื่อหลีกเลี่ยงปริมาณการใช้เครือข่ายที่ไม่จำเป็นและผลข้างเคียงที่อาจเกิดขึ้น
- การจัดการ Timeout: คุณตั้งค่าการหมดเวลาสำหรับการทำงานแบบอะซิงโครนัส หากการทำงานเสร็จสิ้นก่อนที่เวลาจะหมด คุณควรยกเลิกการหมดเวลานั้นเพื่อป้องกันการทำงานของโค้ดที่ซ้ำซ้อน
- การยกเลิก Component (Component Unmounting): ในเฟรมเวิร์ก front-end เช่น React หรือ Vue.js, component มักจะทำการร้องขอแบบอะซิงโครนัส เมื่อ component ถูก unmount การร้องขอใดๆ ที่กำลังดำเนินอยู่ซึ่งเกี่ยวข้องกับ component นั้นควรถูกยกเลิกเพื่อหลีกเลี่ยงหน่วยความจำรั่วไหล (memory leaks) และข้อผิดพลาดที่เกิดจากการอัปเดต component ที่ถูก unmount ไปแล้ว
- ข้อจำกัดด้านทรัพยากร: ในสภาพแวดล้อมที่มีทรัพยากรจำกัด (เช่น อุปกรณ์มือถือ, ระบบฝังตัว) การยกเลิกการทำงานที่ไม่จำเป็นสามารถเพิ่มทรัพยากรที่มีค่าและปรับปรุงประสิทธิภาพได้ ตัวอย่างเช่น การยกเลิกการดาวน์โหลดรูปภาพขนาดใหญ่หากผู้ใช้เลื่อนผ่านส่วนนั้นของหน้าไปแล้ว
แนะนำ AbortController และ AbortSignal
อินเทอร์เฟซ AbortController
ถูกออกแบบมาเพื่อแก้ปัญหาการยกเลิกการทำงานแบบอะซิงโครนัส ประกอบด้วยสองส่วนประกอบสำคัญ:
- AbortController: อ็อบเจกต์นี้จัดการสัญญาณการยกเลิก มีเมธอดเดียวคือ
abort()
ซึ่งใช้เพื่อส่งสัญญาณขอยกเลิก - AbortSignal: อ็อบเจกต์นี้แทนสัญญาณว่าการทำงานควรถูกยกเลิก มันเชื่อมโยงกับ
AbortController
และจะถูกส่งไปยังการทำงานแบบอะซิงโครนัสที่ต้องการให้สามารถยกเลิกได้
การใช้งานพื้นฐาน: การยกเลิก Fetch Requests
มาเริ่มด้วยตัวอย่างง่ายๆ ของการยกเลิก fetch
request:
const controller = new AbortController();
const signal = controller.signal;
fetch('https://api.example.com/data', { signal })
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('Data:', data);
})
.catch(error => {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Fetch error:', error);
}
});
// To cancel the fetch request:
controller.abort();
คำอธิบาย:
- เราสร้าง instance ของ
AbortController
- เราดึง
AbortSignal
ที่เกี่ยวข้องมาจากcontroller
- เราส่ง
signal
ไปยัง options ของfetch
- หากเราต้องการยกเลิก request เราจะเรียกใช้
controller.abort()
- ในบล็อก
.catch()
เราตรวจสอบว่า error เป็นAbortError
หรือไม่ ถ้าใช่ เราจะรู้ว่า request ถูกยกเลิกแล้ว
การจัดการ AbortError
เมื่อ controller.abort()
ถูกเรียก, fetch
request จะถูกปฏิเสธ (rejected) ด้วย AbortError
การจัดการ error นี้อย่างเหมาะสมในโค้ดของคุณเป็นสิ่งสำคัญอย่างยิ่ง การไม่ทำเช่นนั้นอาจนำไปสู่ unhandled promise rejections และพฤติกรรมที่ไม่คาดคิด
นี่คือตัวอย่างที่แข็งแกร่งขึ้นพร้อมการจัดการ error:
const controller = new AbortController();
const signal = controller.signal;
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data', { signal });
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
console.log('Data:', data);
return data;
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
return null; // Or throw the error to be handled further up
} else {
console.error('Fetch error:', error);
throw error; // Re-throw the error to be handled further up
}
}
}
fetchData();
// To cancel the fetch request:
controller.abort();
แนวทางปฏิบัติที่ดีที่สุดสำหรับการจัดการ AbortError:
- ตรวจสอบชื่อ error: ตรวจสอบเสมอว่า
error.name === 'AbortError'
เพื่อให้แน่ใจว่าคุณกำลังจัดการกับประเภท error ที่ถูกต้อง - คืนค่าเริ่มต้นหรือโยน error ต่อ (re-throw): ขึ้นอยู่กับตรรกะของแอปพลิเคชันของคุณ คุณอาจต้องการคืนค่าเริ่มต้น (เช่น
null
) หรือโยน error ต่อไปเพื่อให้จัดการในลำดับชั้นการเรียกที่สูงขึ้น - ทำความสะอาดทรัพยากร: หากการทำงานแบบอะซิงโครนัสมีการจัดสรรทรัพยากรใดๆ (เช่น timers, event listeners) ให้ทำความสะอาดทรัพยากรเหล่านั้นใน handler ของ
AbortError
การยกเลิก Timers ด้วย AbortSignal
AbortSignal
ยังสามารถใช้เพื่อยกเลิก timers ที่สร้างด้วย setTimeout
หรือ setInterval
ได้ด้วย ซึ่งต้องมีการทำงานด้วยตนเองเพิ่มขึ้นเล็กน้อย เนื่องจากฟังก์ชัน timer ที่มีอยู่แล้วไม่รองรับ AbortSignal
โดยตรง คุณต้องสร้างฟังก์ชันแบบกำหนดเองที่คอยฟัง abort signal และล้าง timer เมื่อมันถูกเรียกใช้
function cancellableTimeout(callback, delay, signal) {
let timeoutId;
const timeoutPromise = new Promise((resolve, reject) => {
timeoutId = setTimeout(() => {
resolve(callback());
}, delay);
signal.addEventListener('abort', () => {
clearTimeout(timeoutId);
reject(new Error('Timeout Aborted'));
});
});
return timeoutPromise;
}
const controller = new AbortController();
const signal = controller.signal;
cancellableTimeout(() => {
console.log('Timeout executed');
}, 2000, signal)
.then(() => console.log("Timeout finished successfully"))
.catch(err => console.log(err));
// To cancel the timeout:
controller.abort();
คำอธิบาย:
- ฟังก์ชัน
cancellableTimeout
รับ callback, delay, และAbortSignal
เป็นอาร์กิวเมนต์ - มันจะตั้งค่า
setTimeout
และเก็บ ID ของ timeout ไว้ - มันจะเพิ่ม event listener ไปยัง
AbortSignal
เพื่อคอยฟังเหตุการณ์abort
- เมื่อเหตุการณ์
abort
ถูกกระตุ้น, event listener จะล้าง timeout และปฏิเสธ promise
การยกเลิก Event Listeners
เช่นเดียวกับ timers คุณสามารถใช้ AbortSignal
เพื่อยกเลิก event listeners ได้ ซึ่งมีประโยชน์อย่างยิ่งเมื่อคุณต้องการลบ event listeners ที่เกี่ยวข้องกับ component ที่กำลังจะถูก unmount
const controller = new AbortController();
const signal = controller.signal;
const button = document.getElementById('myButton');
button.addEventListener('click', () => {
console.log('Button clicked!');
}, { signal });
// To cancel the event listener:
controller.abort();
คำอธิบาย:
- เราส่ง
signal
เป็น option ไปยังเมธอดaddEventListener
- เมื่อ
controller.abort()
ถูกเรียก, event listener จะถูกลบออกโดยอัตโนมัติ
AbortController ใน React Components
ใน React คุณสามารถใช้ AbortController
เพื่อยกเลิกการทำงานแบบอะซิงโครนัสเมื่อ component ถูก unmount นี่เป็นสิ่งจำเป็นเพื่อป้องกันหน่วยความจำรั่วไหลและข้อผิดพลาดที่เกิดจากการอัปเดต component ที่ถูก unmount ไปแล้ว นี่คือตัวอย่างการใช้ useEffect
hook:
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data', { signal });
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
setData(data);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Fetch error:', error);
}
}
}
fetchData();
return () => {
controller.abort(); // Cancel the fetch request when the component unmounts
};
}, []); // Empty dependency array ensures this effect runs only once on mount
return (
{data ? (
Data: {JSON.stringify(data)}
) : (
Loading...
)}
);
}
export default MyComponent;
คำอธิบาย:
- เราสร้าง
AbortController
ภายในuseEffect
hook - เราส่ง
signal
ไปยังfetch
request - เราคืนค่าฟังก์ชัน cleanup จาก
useEffect
hook ฟังก์ชันนี้จะถูกเรียกเมื่อ component ถูก unmount - ภายในฟังก์ชัน cleanup เราเรียก
controller.abort()
เพื่อยกเลิก fetch request
กรณีการใช้งานขั้นสูง
การเชื่อมต่อ AbortSignals
บางครั้ง คุณอาจต้องการเชื่อมต่อ AbortSignal
หลายๆ ตัวเข้าด้วยกัน ตัวอย่างเช่น คุณอาจมี parent component ที่ต้องการยกเลิกการทำงานใน child components ของมัน คุณสามารถทำได้โดยการสร้าง AbortController
ใหม่และส่ง signal ของมันไปยังทั้ง parent และ child components
การใช้ AbortController กับไลบรารีของบุคคลที่สาม
หากคุณกำลังใช้ไลบรารีของบุคคลที่สามที่ไม่รองรับ AbortSignal
โดยตรง คุณอาจต้องปรับโค้ดของคุณให้ทำงานกับกลไกการยกเลิกของไลบรารีนั้นๆ ซึ่งอาจเกี่ยวข้องกับการห่อหุ้มฟังก์ชันอะซิงโครนัสของไลบรารีด้วยฟังก์ชันของคุณเองที่จัดการกับ AbortSignal
ประโยชน์ของการใช้ AbortController
- ประสิทธิภาพที่ดีขึ้น: การยกเลิกการทำงานที่ไม่จำเป็นสามารถลดปริมาณการใช้เครือข่าย, การใช้ CPU และการใช้หน่วยความจำ ซึ่งนำไปสู่ประสิทธิภาพที่ดีขึ้น โดยเฉพาะอย่างยิ่งบนอุปกรณ์ที่มีทรัพยากรจำกัด
- โค้ดที่สะอาดขึ้น:
AbortController
เป็นวิธีที่เป็นมาตรฐานและสวยงามในการจัดการการยกเลิก ทำให้โค้ดของคุณอ่านง่ายและบำรุงรักษาได้ง่ายขึ้น - การป้องกันหน่วยความจำรั่วไหล: การยกเลิกการทำงานแบบอะซิงโครนัสที่เกี่ยวข้องกับ component ที่ถูก unmount จะช่วยป้องกันหน่วยความจำรั่วไหลและข้อผิดพลาดที่เกิดจากการอัปเดต component ที่ถูก unmount ไปแล้ว
- ประสบการณ์ผู้ใช้ที่ดีขึ้น: การยกเลิกการร้องขอที่ไม่เกี่ยวข้องสามารถปรับปรุงประสบการณ์ของผู้ใช้โดยป้องกันการแสดงข้อมูลที่ล้าสมัยและลดความล่าช้าที่ผู้ใช้รับรู้
ความเข้ากันได้ของเบราว์เซอร์
AbortController
ได้รับการสนับสนุนอย่างกว้างขวางในเบราว์เซอร์สมัยใหม่ รวมถึง Chrome, Firefox, Safari และ Edge คุณสามารถตรวจสอบตารางความเข้ากันได้บน MDN Web Docs สำหรับข้อมูลล่าสุด
Polyfills
สำหรับเบราว์เซอร์รุ่นเก่าที่ไม่รองรับ AbortController
โดยกำเนิด คุณสามารถใช้ polyfill ได้ polyfill คือโค้ดชิ้นหนึ่งที่ให้ฟังก์ชันการทำงานของคุณสมบัติใหม่ๆ ในเบราว์เซอร์รุ่นเก่า มี AbortController
polyfills หลายตัวให้เลือกใช้ออนไลน์
สรุป
อินเทอร์เฟซ AbortController
เป็นเครื่องมือที่มีประสิทธิภาพสำหรับการจัดการการทำงานแบบอะซิงโครนัสใน JavaScript ด้วยการใช้ AbortController
คุณสามารถเขียนโค้ดที่สะอาดขึ้น, มีประสิทธิภาพมากขึ้น และแข็งแกร่งขึ้น ซึ่งจัดการกับการยกเลิกได้อย่างราบรื่น ไม่ว่าคุณจะดึงข้อมูลจาก API, ตั้งเวลา หรือจัดการ event listeners, AbortController
สามารถช่วยคุณปรับปรุงคุณภาพโดยรวมของเว็บแอปพลิเคชันของคุณได้