إتقان JavaScript و TypeScript
الأنماط غير المتزامنة ومعالجة الأخطاء
4 دقيقة للقراءة
البرمجة غير المتزامنة هي حيث يتعثر كثير من المرشحين. يختبر المحاورون معرفتك بواجهات Promise وقدرتك على التعامل مع الحالات الحدية بأمان.
مُجمّعات Promise
اعرف الأربعة جميعًا ومتى تستخدم كلاً منها:
const fast = new Promise(resolve => setTimeout(() => resolve('fast'), 100));
const slow = new Promise(resolve => setTimeout(() => resolve('slow'), 500));
const fail = new Promise((_, reject) => setTimeout(() => reject('error'), 200));
// Promise.all — تُحل عندما تُحل الكل، ترفض عند أول رفض
await Promise.all([fast, slow]); // ['fast', 'slow']
await Promise.all([fast, fail, slow]); // ترمي 'error'
// Promise.allSettled — تنتظر استقرار الكل (لا ترفض أبدًا)
await Promise.allSettled([fast, fail, slow]);
// [
// { status: 'fulfilled', value: 'fast' },
// { status: 'rejected', reason: 'error' },
// { status: 'fulfilled', value: 'slow' }
// ]
// Promise.race — تُحل/ترفض مع أول من يستقر
await Promise.race([fast, slow]); // 'fast'
// Promise.any — تُحل مع أول من يُنجز (تتجاهل الرفض)
await Promise.any([fail, fast, slow]); // 'fast'
await Promise.any([fail]); // ترمي AggregateError
| المُجمّع | تُحل عندما | ترفض عندما |
|---|---|---|
all |
تُنجز الكل | أول رفض |
allSettled |
تستقر الكل | لا ترفض أبدًا |
race |
أول من يستقر | أول رفض (إذا كان أول من يستقر) |
any |
أول من يُنجز | ترفض الكل (AggregateError) |
تنفيذ Promise.all
سؤال برمجة شائع في المقابلات:
function promiseAll(promises) {
return new Promise((resolve, reject) => {
if (promises.length === 0) {
resolve([]);
return;
}
const results = new Array(promises.length);
let resolvedCount = 0;
promises.forEach((promise, index) => {
Promise.resolve(promise).then(
(value) => {
results[index] = value;
resolvedCount++;
if (resolvedCount === promises.length) {
resolve(results);
}
},
(error) => {
reject(error);
}
);
});
});
}
التفاصيل الرئيسية التي يتحقق منها المحاورون:
- النتائج يجب أن تحافظ على الترتيب (استخدم
index، وليس push) - معالجة القيم غير الـ Promise عبر
Promise.resolve(promise) - معالجة حالة المصفوفة الفارغة
- استدعاء
resolveمرة واحدة فقط (عند اكتمال الكل) - الرفض فورًا عند أول خطأ
AbortController
يُستخدم لإلغاء طلبات fetch والعمليات غير المتزامنة الأخرى:
async function fetchWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`انتهت مهلة الطلب بعد ${timeoutMs}ms`);
}
throw error;
}
}
// يمكن لـ AbortController أيضًا إلغاء عمليات متعددة
const controller = new AbortController();
// تمرير نفس الإشارة لعدة استدعاءات fetch
const [users, posts] = await Promise.all([
fetch('/api/users', { signal: controller.signal }),
fetch('/api/posts', { signal: controller.signal }),
]);
// إلغاء الاثنين إذا فُكّ المكون (نمط React)
controller.abort();
إعادة المحاولة مع التراجع الأسي
نمط عملي يجمع مفاهيم async:
async function retry(fn, maxRetries = 3, baseDelay = 1000) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = baseDelay * Math.pow(2, attempt);
const jitter = delay * (0.5 + Math.random() * 0.5);
await new Promise(resolve => setTimeout(resolve, jitter));
}
}
}
// الاستخدام
const data = await retry(
() => fetch('/api/data').then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
}),
3, // 3 محاولات كحد أقصى
1000 // البدء بتأخير 1 ثانية
);
المزالق الشائعة في البرمجة غير المتزامنة
المزلق 1: forEach لا تنتظر await
// خطأ — تطلق جميع الطلبات في نفس الوقت، لا تنتظر
const ids = [1, 2, 3];
ids.forEach(async (id) => {
await fetch(`/api/items/${id}`);
});
console.log('Done'); // يعمل قبل اكتمال الطلبات
// صحيح — تسلسلي
for (const id of ids) {
await fetch(`/api/items/${id}`);
}
console.log('Done'); // يعمل بعد كل الطلبات
// صحيح — متوازي
await Promise.all(ids.map(id => fetch(`/api/items/${id}`)));
console.log('Done'); // يعمل بعد كل الطلبات
المزلق 2: رفض Promise غير المعالج
// خطأ — بدون catch، رفض غير معالج
async function loadData() {
const data = await fetch('/api/data');
return data.json();
}
loadData(); // إذا رُفضت، لا شيء يلتقطها
// صحيح — دائمًا عالج الرفض
loadData().catch(console.error);
// أو في React، استخدم error boundaries + تنظيف useEffect
useEffect(() => {
let cancelled = false;
loadData().then(data => {
if (!cancelled) setData(data);
}).catch(err => {
if (!cancelled) setError(err);
});
return () => { cancelled = true; };
}, []);
المزلق 3: حالات السباق في React
// خطأ — بيانات قديمة إذا اكتملت الطلبات بترتيب خاطئ
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
fetch(`/api/search?q=${query}`)
.then(r => r.json())
.then(setResults);
}, [query]);
// إذا تغير query بسرعة: طلب "ab" قد يُحل
// بعد طلب "abc"، مما يعرض نتائج خاطئة
}
// صحيح — إلغاء الطلبات القديمة
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
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]);
}
نصيحة للمقابلات: عند مناقشة الكود غير المتزامن، اذكر دائمًا معالجة الأخطاء والإلغاء وحالات السباق. هذا يُظهر تفكيرًا على مستوى الإنتاج، وليس مجرد معرفة كتب.
الآن حان الوقت لتطبيق هذه المفاهيم عمليًا مع مختبر محاكي محرك JavaScript. :::