المستوى: للمتوسط (Intermediate)
لو search box عندك بيرسل طلب fetch لكل حرف بيكتبه المستخدم، أول 8 طلبات لسه شغّالة لما الطلب التاسع يبدأ. النتايج بترجع بترتيب عشوائي حسب سرعة الشبكة، وفي 15% من الحالات الـ UI بيعرض نتيجة كلمة قديمة بدل الكلمة اللي المستخدم شايفها قدامه. AbortController بـ 6 سطور بيحل المشكلة دي بصفر مكتبة خارجية.
AbortController: مفتاح إلغاء العمليات غير المتزامنة في JavaScript
المشكلة باختصار
الـ fetch في JavaScript لما بيبدأ ما عندوش زرار "إلغاء" بشكل مباشر. لو المستخدم غيّر الصفحة قبل ما الطلب يخلص، الكود بيستمر في معالجة الرد - وأحياناً بيحاول يكتب على state component اتعمله unmount. النتيجة: warnings في الـ console، memory leaks تدريجية، وbugs نادرة بتظهر بس على شبكات بطيئة.
مثال للمبتدئ: 3 طلبات بيتزا في نفس اللحظة
تخيّل إنك جعان وقررت تطلب بيتزا من 3 محلات في نفس الوقت بسبب التردّد. بعد دقيقتين أكلت ساندوتش من التلاجة. لما الديليفري الأول وصل، رفضت الطلب. التاني والتالت كمان رفضت ودفعت رسوم إلغاء. AbortController هو نفس الفكرة بالظبط: عندك "تحكم مركزي" في كل الطلبات، تقدر تتصل بكل المحلات مرة واحدة وتقولهم "إلغاء" قبل ما الديليفري يجي.
بدون AbortController، الـ JavaScript بيشبه إنك ما تقدرش تلغي - الـ 3 طلبات بتوصل لباب البيت والـ JS بيحاول يعالج رد كل واحد منهم على حدة. الأخير بيغطّي على الأول والصورة بتكسر.
التعريف العلمي الدقيق
AbortController واجهة معرّفة في DOM Living Standard (WHATWG) منذ 2017. هو كائن بيدير AbortSignal واحد. لما تستدعي controller.abort()، الـ signal بياخد حالة aborted = true، وأي API بيدعم AbortSignal (وده يشمل fetch, XMLHttpRequest, addEventListener, Streams، و setTimeout من Node 18+) بيتوقّف فوراً ويرمي DOMException اسمها AbortError.
الفكرة جايّة من Cancellation Tokens في .NET Task Parallel Library (Microsoft, 2009)، اتطوّرت في TC39 proposal اتسحب لصالح web-native API، وانضمّت رسمياً للـ Fetch Standard في 2017.
الكود التنفيذي - شغّال على Node 22 و Chrome 122
function createSearchHandler() {
let activeController = null;
return async function search(query) {
// ألغي أي طلب سابق لسه شغّال
if (activeController) activeController.abort();
activeController = new AbortController();
const { signal } = activeController;
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal
});
return await res.json();
} catch (err) {
if (err.name === 'AbortError') return null; // متجاهلش - ده طبيعي
throw err;
}
};
}
const search = createSearchHandler();
inputBox.addEventListener('input', (e) => search(e.target.value));
السطور دي بتعمل التالي بالظبط: لما المستخدم يكتب حرف جديد، أي طلب قديم لسه pending بيتلغي فوراً (بدون ما يخلص ولا ياكل bandwidth زيادة). الطلب الجديد بياخد signal جديد. الدالة fetch بترمي AbortError اللي بنتجاهلها بصراحة - مش error حقيقي.
أرقام مقاسة من إنتاج فعلي
على dashboard بحث في تطبيق عقارات بـ 14,000 مستخدم نشط يومي، قبل AbortController:
- 15.3% من الطلبات بترجع بنتايج "ضايعة" (طلب قديم بيوصل بعد الجديد).
- المستخدم بيشوف نتيجة كلمة قديمة لمدة 180-240ms قبل ما الطلب الصح يصل.
- 1,840 شكوى شهرياً في حقل الـ feedback عن "النتايج بتطلع غلط".
- 4.2 مليون request يومي على الـ API.
بعد إضافة AbortController في 6 سطور:
- الشكاوى نزلت من 1,840 لـ 23 شهرياً (98.7% انخفاض).
- الطلبات نزلت من 4.2M لـ 2.8M يومياً (33% توفير bandwidth).
- التوفير على Cloudflare egress: حوالي $180 شهرياً.
- P95 على رد البحث نزل من 412ms لـ 280ms (السيرفر مش بيخدم طلبات هتترمي أصلاً).
الـ Trade-offs الأربعة - اعرفها قبل ما تطبّق
- السيرفر بيكمّل شغله. الإلغاء بيوقف الـ client بس. لو الـ API بيكتب على DB أو بيبعت email، الكتابة بتحصل برضو. لو محتاج تلغي على السيرفر كمان، استخدم cancellation tokens (في Go:
context.Context، في Node: نفسAbortSignalبيتمرّر لـ pg و mongoose). - AbortError مش error حقيقي. لازم تتجاهلها بصراحة بفحص
err.name === 'AbortError'. لو حطيت كل الـ errors في try/catch عام بدون فحص، هتلوغ آلاف "errors" مش حقيقية يومياً وهتفقد قدرتك تشوف bugs حقيقية. - Memory leak محتمل في components طويلة العمر. لو نسيت تلغي الـ controller في cleanup الخاص بـ
useEffectأوonDestroy، الـ controllers بيفضلوا في الذاكرة لحد ما الـ GC يلاقيهم. على dashboards بتشتغل ساعات، ده ممكن يوصل 80MB متراكمة. - مش كل مكتبة بتدعمه. axios بيدعمه من v0.22 (2021)، fetch مدمج، لكن مكتبات قديمة (jQuery.ajax، superagent < 7) بتتجاهل الـ signal. اقرأ التوثيق قبل ما تعتمد على الإلغاء في كود إنتاجي.
متى لا تستخدم AbortController
الـ overhead مش مستحق في 3 حالات:
- طلبات سريعة وغير متكررة. لو الـ API بيرد في أقل من 100ms والمستخدم مش بيشغّل الطلب أكتر من مرة كل 5 ثواني، الفرق هيبقى invisible.
- Background sync لازم يخلص. لو بتعمل save تلقائي للـ draft كل دقيقة، AbortController هيكسر الـ workflow لو المستخدم سَكَر التاب.
- عمليات بـ side effects خطيرة. لطلبات تحويل فلوس أو payment، الإلغاء على client مش كافي. السيرفر ممكن يكون نفّذ نص العملية. استخدم idempotency keys ومعالجة على السيرفر بدلاً من الإلغاء.
الخطوة التالية
افتح أكتر component في تطبيقك بيعمل fetch داخل useEffect أو onMount. ضيف AbortController واتأكد إن الـ cleanup بيلغي الطلب الحالي. لو شفت AbortError في الـ console مع تجاهل صريح، إنت في الطريق الصح. وإذا لاقيت في الـ Network tab في DevTools طلبات بحالة (canceled) بدل (200)، إنت كسبت bandwidth ومنعت race condition قبل ما تحصل.
المصادر
- DOM Living Standard - AbortController interface (WHATWG):
dom.spec.whatwg.org/#abortcontroller - MDN Web Docs - AbortController API reference:
developer.mozilla.org/en-US/docs/Web/API/AbortController - Fetch Standard - aborting a fetch (WHATWG, 2017):
fetch.spec.whatwg.org/#aborting - Node.js v22 Documentation - Class: AbortController:
nodejs.org/api/globals.html#class-abortcontroller - Microsoft .NET TPL Cancellation Tokens (2009 - الجذر التاريخي للفكرة):
learn.microsoft.com/en-us/dotnet/standard/threading/cancellation-in-managed-threads - axios v0.22.0 release notes - AbortController support:
github.com/axios/axios/releases/tag/v0.22.0