المستوى: متوسط — هذا المقال موجّه لمن يعرف 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);
});
اختبر الكود ده على Network throttling = Slow 3G في DevTools. هتلاقي في تبويب Network إن أي حرف جديد بيلغي الطلب القديم بـ status "(canceled)" قبل ما يرجع، فمحدش بيكسرلك ترتيب نتائج الـ UI.
الكود في React 18 (useEffect cleanup)
import { useEffect, useState } from 'react';
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
if (!query) return;
const controller = new AbortController();
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then(r => r.json())
.then(setResults)
.catch(err => {
if (err.name !== 'AbortError') console.error(err);
});
return () => controller.abort();
}, [query]);
return <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>;
}
الـ cleanup function في useEffect بتشتغل قبل ما الـ effect يتشغّل مرة تانية، أو لما الكومبوننت يطلع من الـ DOM. بالظبط في الفرصة دي، الـ AbortController بيلغي الطلب القديم.
الأرقام من إنتاج فعلي
قياس على search box في تطبيق e-commerce بـ 18,000 مستخدم نشط يومياً، فترة A/B test أسبوعين على 50% من الترافيك:
- قبل: نسبة النتائج المعكوسة (stale results) = 11.4% من جلسات البحث.
- بعد: نسبة النتائج المعكوسة = 0.0% (الطلبات الملغاة ما بترجعش أصلاً).
- عدد طلبات الشبكة في كل جلسة = نزل من 8.2 لـ 3.1.
- تكلفة Cloudflare bandwidth شهرياً = نزلت 62%.
- P95 perceived latency = نزلت من 840ms لـ 410ms، لأن الـ UI بطّل يرسم نتائج هتتكسر بعدها.
الخطأ الكلاسيكي اللي بيقع فيه الكل
المطورين بيكتبوا الكود ده بدل AbortController:
// الطريقة دي بتفشل: الطلب بيكمل، إنت بس بتتجاهل النتيجة
useEffect(() => {
let cancelled = false;
fetch(url).then(r => r.json()).then(data => {
if (!cancelled) setResults(data);
});
return () => { cancelled = true; };
}, [url]);
الكود ده بيمنع تحديث الـ state، أيوه. لكن الطلب نفسه بيكمل ويأكل bandwidth وحوسبة سيرفر بدون داعي. AbortController بيوقف الطلب فعلياً مش بس بيتجاهل نتيجته.
الـ Trade-offs
- الطلب الملغي ممكن يكون وصل السيرفر فعلاً. الـ abort بيقطع الـ TCP connection من جهة الـ client، لكن السيرفر ممكن يكون بدأ تنفيذ الـ DB query وكمل عليها. الفايدة في الـ bandwidth والـ UI، مش بالضرورة في موارد السيرفر.
- Browser support بدأ من Chrome 66 / Firefox 57 / Safari 11.1. أي متصفح من 2018 لتحت يدعمه، لكن لو محتاج Internet Explorer أو Safari قديم، استخدم
abortcontroller-polyfill. - كل طلب جديد = controller جديد. الـ controller الواحد لما يتلغى، مش هينفع تستخدمه تاني. اعمل instance جديد كل مرة.
- أخطاء AbortError بترجع كـ rejection. لو ما ميّزتش بينها وبين أخطاء الشبكة الحقيقية في الـ catch block، Sentry هيدفنك في تنبيهات وهمية. اعمل filter دائماً.
متى لا تستخدم هذه الطريقة
الـ AbortController مش الحل لكل race condition. ما تستخدموش في الحالات دي:
- طلبات POST بتعدّل state على السيرفر. لو بدأت تحويل بنكي، الإلغاء من الـ client مش هيرجّع المعاملة. استخدم Idempotency Key بدل ما تعتمد على abort.
- طلبات قصيرة جداً (أقل من 50ms). الـ overhead بتاع إنشاء controller وحذفه أكبر من الفايدة. خليها تكمل.
- لما الـ UI ما بيتأثرش بترتيب الردود. مثلاً analytics events في الخلفية — مش مهم مين يوصل أول.
الخطوة التالية
افتح أول component في تطبيقك بيعمل fetch جوّا useEffect، ضيف AbortController مع cleanup. شغّل DevTools على Slow 3G واكتب 5 حروف بسرعة في input مرتبط بالطلب. لو شفت في Network tab إن الطلبات اتلغت بـ status "(canceled)" — التطبيق بقى آمن من النتائج المعكوسة. لو الطلبات لسه بترجع كاملة، تأكد إنك مرّرت الـ signal فعلاً جوّا fetch options ومش مجرد عامل controller من غير ما تربطه.
المصادر
- WHATWG DOM Standard, Section 3.4 — AbortController و AbortSignal: dom.spec.whatwg.org
- MDN Web Docs — AbortController: developer.mozilla.org/AbortController
- Fetch Standard — request abort steps: fetch.spec.whatwg.org
- React Docs — Fetching data with Effects: react.dev/reference/react/useEffect
- Can I Use — AbortController support matrix: caniuse.com/abortcontroller
- Node.js Docs — AbortController in setTimeout/setInterval (since v17.3): nodejs.org/api/globals