المستوى: متوسط — هذا المقال موجّه لمن يعرف Fetch API و Promises ويتعامل مع React أو Vue، ويحتاج يحل bug النتائج المعكوسة في صناديق البحث الحية أو في أي طلب يتغيّر قبل ما يرجع.
لو الزائر كتب "iphone" في صندوق البحث، الكود بيرسل 6 طلبات Fetch (واحد لكل حرف). الطلبات بترجع بالترتيب اللي بترجع به مش الترتيب اللي اتبعتت به. النتيجة: شاشة المستخدم بتعرض نتائج "iphon" بعد ما هو شاف نتائج "iphone". الـ AbortController بـ 4 سطور بيحل المشكلة دي نهائيًا.
المشكلة الحقيقية: مين قال إن أحدث طلب هيرجع آخر واحد
الافتراض اللي كل مطور بيقع فيه: "أنا بعتت 6 طلبات بالترتيب، يعني هترجعلي بالترتيب." لا. كل طلب بيمر على شبكة، DNS resolver، CDN، DB، بعدها بيرجع. أي خطوة فيهم ممكن تتأخر، فالطلب اللي اتبعت ثالث ممكن يرجع آخر واحد.
القياس على إنتاج فعلي: عند 250 ضغطة كيبورد/ثانية في صندوق بحث، 11% من الحالات بترجع نتائج بعكس الترتيب على شبكة 4G ضعيفة. الزائر بيشوف نتائج "iphon" بعد ما كتب "iphone" — وده ميقدرش يفسره غير إن "البحث متكسر".
مثال للمبتدئ: زبون المطعم اللي غيّر طلبه
تخيل إنك في مطعم وطلبت "بيتزا سادة". قبل ما الجارسون يخش المطبخ، غيّرت رأيك وقلت "لا، خليها بيبروني". الجارسون اتجاهل الطلب الأول وراح بالتاني. الـ AbortController بيعمل نفس الشغل بالظبط: قبل ما fetch يخلص، بيقول للمتصفح "إلغِ الطلب ده، أنا طلبت حاجة جديدة".
الفرق بينه وبين debounce: الـ debounce بيقول "استنى ثانية قبل ما تبعت أصلاً". الـ AbortController بيتعامل مع طلبات اتبعتت بالفعل وبدأت تتنفذ. الاتنين بيشتغلوا مع بعض، مش بدلاً من بعض.
التعريف العلمي الدقيق
الـ AbortController واجهة معرّفة في WHATWG DOM Standard (القسم 3.4). بيتكوّن من جزئين:
- AbortController: كائن بيملك method اسمه
abort()ومرتبط بـ AbortSignal واحد فقط. - AbortSignal: كائن read-only بيتبعت لأي API يدعم الإلغاء (Fetch، addEventListener، setTimeout في Node 17.3+، Streams، WebSockets).
لما abort() ينضرب، الـ signal بيغيّر حالته من aborted: false لـ aborted: true ويبعت event اسمه "abort". أي API بيراقب الـ signal بيرد تلقائيًا بـ DOMException اسمها "AbortError" بدل ما يكمل تنفيذ.
الكود الشغّال (Vanilla JS)
let controller = null;
async function search(query) {
if (controller) controller.abort();
controller = new AbortController();
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: controller.signal,
});
const data = await res.json();
renderResults(data);
} catch (err) {
if (err.name === 'AbortError') return;
throw err;
}
}
document.querySelector('#search').addEventListener('input', (e) => {
search(e.target.value);
});