AbortController: ألغِ طلبات fetch القديمة قبل ما تسبقك
مستوى القارئ: متوسط
هتمنع واجهة البحث من عرض نتائج قديمة، وهتقلل الضغط على الـ API، بدل ما كل حرف يفتح طلب جديد يفضل شغال لحد ما يرجع.
المشكلة باختصار
ركز في سيناريو autocomplete بسيط. المستخدم كتب s ثم se ثم sea بسرعة. الواجهة أرسلت 3 طلبات. الطبيعي إن آخر طلب هو المهم، لكن اللي بيحصل فعلاً إن الطلب الأول ممكن يرجع بعد الثالث بسبب الشبكة أو الكاش أو ضغط السيرفر. النتيجة: المستخدم كتب كلمة أحدث، والواجهة تعرض نتيجة أقدم.
الطريقة الشائعة الغلط هي الاعتماد على debounce فقط. الـ debounce مفيد، لكنه لا يلغي طلبًا خرج بالفعل. لو الطلب وصل للسيرفر، سيظل يستهلك وقتًا وذاكرة واتصالًا. الحل هنا هو AbortController: إشارة إلغاء تمررها إلى fetch، ثم تستدعي abort() قبل إرسال طلب جديد.
مثال بسيط قبل التعريف العلمي
اعتبر إن عندك موظف دعم يرد على آخر رسالة من العميل. العميل بعت 8 رسائل وراء بعض، والمهم هو آخر رسالة فقط. لو الموظف فضل يرد على أول رسالة، الرد هيبقى صحيح لغويًا لكنه غير مناسب للحالة الحالية. AbortController يعمل نفس الفكرة للطلبات غير المهمة: أول ما طلب أحدث يظهر، الطلب القديم يأخذ إشارة توقف.
علميًا، AbortController كائن يولد AbortSignal. هذه الإشارة تمر إلى واجهات غير متزامنة مثل fetch. عند استدعاء abort()، يتم رفض وعد fetch عادة بخطأ اسمه AbortError، وتقدر تتجاهله لأنه إلغاء مقصود وليس فشلًا حقيقيًا.
الحل العملي في JavaScript
الافتراض إن عندك input بحث يضرب endpoint مثل /api/products?q=.... الهدف: طلب واحد نشط فقط لكل مستخدم في نفس خانة البحث.
let searchController = null;
async function searchProducts(query) {
if (searchController) {
searchController.abort();
}
searchController = new AbortController();
try {
const response = await fetch(`/api/products?q=${encodeURIComponent(query)}`, {
signal: searchController.signal,
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const products = await response.json();
renderProducts(products);
} catch (error) {
if (error.name === 'AbortError') return;
showSearchError('البحث فشل. جرّب مرة تانية.');
}
}
const input = document.querySelector('#product-search');
input.addEventListener('input', (event) => {
const query = event.target.value.trim();
if (query.length < 2) return;
searchProducts(query);
});