أنماط متقدمة للبرمجة غير المتزامنة
بعد أساسيات async/await، توفر JavaScript أنماطاً قوية للتعامل مع السيناريوهات غير المتزامنة المعقدة. في هذا الدرس، سنستكشف التكرار غير المتزامن والمولدات واستراتيجيات التنفيذ المتوازي وتقنيات معالجة الأخطاء المتقدمة.
التكرار غير المتزامن مع for await...of
تسمح لك حلقة for await...of بالتكرار عبر القوابل للتكرار غير المتزامنة، ومعالجة Promises بشكل متسلسل:
// مثال قابل للتكرار غير متزامن
async function* generateNumbers() {
for (let i = 1; i <= 5; i++) {
await new Promise(resolve => setTimeout(resolve, 500));
yield i;
}
}
// استخدام for await...of
async function processNumbers() {
for await (const num of generateNumbers()) {
console.log(num); // 1, 2, 3, 4, 5 (واحد كل 500 ملي ثانية)
}
}
processNumbers();
// مثال من العالم الحقيقي: معالجة صفحات API
async function* fetchAllPages(baseUrl) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${baseUrl}?page=${page}`);
const data = await response.json();
yield data.items;
hasMore = data.hasNextPage;
page++;
}
}
// الاستخدام
async function loadAllItems() {
for await (const items of fetchAllPages("/api/products")) {
console.log(`معالجة ${items.length} عنصر`);
items.forEach(item => console.log(item.name));
}
}
نقطة مهمة: for await...of مثالي لمعالجة تدفقات البيانات أو APIs المقسمة إلى صفحات أو أي سيناريو تحتاج فيه إلى معالجة Promises واحداً تلو الآخر بشكل متسلسل.
المولدات غير المتزامنة
تجمع المولدات غير المتزامنة بين قوة المولدات و async/await، مما يسمح لك بإنتاج Promises:
// مولد غير متزامن بسيط
async function* countDown(start) {
for (let i = start; i >= 0; i--) {
await new Promise(resolve => setTimeout(resolve, 1000));
yield i;
}
}
// الاستخدام
async function runCountdown() {
const countdown = countDown(3);
for await (const num of countdown) {
console.log(num); // 3, 2, 1, 0 (واحد في الثانية)
}
console.log("انتهى!");
}
// متقدم: مولد غير متزامن مع مدخلات
async function* dataProcessor() {
while (true) {
const input = yield; // استقبال مدخلات
if (input === "stop") break;
// معالجة البيانات بشكل غير متزامن
const result = await processData(input);
yield result; // إرسال النتيجة
}
}
async function processData(data) {
await new Promise(resolve => setTimeout(resolve, 100));
return data.toUpperCase();
}
التنفيذ المتوازي مقابل المتسلسل
فهم متى تستخدم التنفيذ المتوازي أو المتسلسل أمر حاسم للأداء:
// التنفيذ المتسلسل (أبطأ)
async function fetchDataSequential() {
console.time("Sequential");
const user = await fetch("/api/user");
const posts = await fetch("/api/posts");
const comments = await fetch("/api/comments");
console.timeEnd("Sequential"); // ~3 ثواني إذا استغرق كل منها 1 ثانية
return {
user: await user.json(),
posts: await posts.json(),
comments: await comments.json()
};
}
// التنفيذ المتوازي (أسرع)
async function fetchDataParallel() {
console.time("Parallel");
// جميع الطلبات تبدأ مرة واحدة
const [user, posts, comments] = await Promise.all([
fetch("/api/user").then(r => r.json()),
fetch("/api/posts").then(r => r.json()),
fetch("/api/comments").then(r => r.json())
]);
console.timeEnd("Parallel"); // ~1 ثانية (الحد الأقصى لجميع الطلبات)
return { user, posts, comments };
}
// مختلط: بعضها متسلسل، وبعضها متوازي
async function fetchDataMixed() {
// الخطوة 1: احصل على المستخدم (مطلوب أولاً)
const user = await fetch("/api/user").then(r => r.json());
// الخطوة 2: احصل على المنشورات والتعليقات بالتوازي (كلاهما يحتاج user.id)
const [posts, comments] = await Promise.all([
fetch(`/api/posts?userId=${user.id}`).then(r => r.json()),
fetch(`/api/comments?userId=${user.id}`).then(r => r.json())
]);
return { user, posts, comments };
}
نصيحة للأداء: استخدم Promise.all() للعمليات المستقلة، لكن كن على دراية بأنه إذا رُفض أي Promise، فإن العملية بأكملها تفشل. استخدم Promise.allSettled() إذا كنت تريد إكمال جميع العمليات بغض النظر عن الإخفاقات الفردية.
تنظيم العمليات غير المتزامنة
تحكم في معدل العمليات غير المتزامنة لتجنب إرهاق APIs أو الموارد:
// دالة تنظيم: حد من العمليات المتزامنة
async function throttlePromises(tasks, limit) {
const results = [];
const executing = [];
for (const task of tasks) {
const promise = task().then(result => {
executing.splice(executing.indexOf(promise), 1);
return result;
});
results.push(promise);
executing.push(promise);
if (executing.length >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
// الاستخدام
const tasks = Array.from({ length: 10 }, (_, i) => {
return async () => {
console.log(`بدء المهمة ${i + 1}`);
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(`اكتملت المهمة ${i + 1}`);
return i + 1;
};
});
// شغل 3 مهام كحد أقصى في وقت واحد
const results = await throttlePromises(tasks, 3);
console.log("جميع النتائج:", results);
تقليل العمليات غير المتزامنة (Debouncing)
منع الاستدعاءات غير المتزامنة المفرطة بالانتظار حتى يتوقف النشاط:
// دالة debounce غير متزامنة
function debounceAsync(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
return new Promise((resolve, reject) => {
timeoutId = setTimeout(async () => {
try {
const result = await func.apply(this, args);
resolve(result);
} catch (error) {
reject(error);
}
}, delay);
});
};
}
// الاستخدام: API بحث مع debounce
const searchAPI = debounceAsync(async (query) => {
console.log(`البحث عن: ${query}`);
const response = await fetch(`/api/search?q=${query}`);
return await response.json();
}, 500);
// كتابة المستخدم تطلق استدعاءات متعددة، لكن الأخيرة فقط تُنفذ
searchInput.addEventListener("input", async (e) => {
try {
const results = await searchAPI(e.target.value);
displayResults(results);
} catch (error) {
console.error("فشل البحث:", error);
}
});
أنماط إعادة المحاولة
أعد محاولة العمليات غير المتزامنة الفاشلة تلقائياً مع تأخير متزايد:
// إعادة المحاولة مع تأخير متزايد
async function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries) {
throw new Error(`فشل بعد ${maxRetries} محاولات: ${error.message}`);
}
const delay = baseDelay * Math.pow(2, attempt - 1);
console.log(`فشلت المحاولة ${attempt}. إعادة المحاولة في ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// الاستخدام
async function fetchUnreliableAPI() {
const response = await fetch("/api/unreliable");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
}
try {
const data = await retryWithBackoff(fetchUnreliableAPI, 3, 1000);
console.log("نجح:", data);
} catch (error) {
console.error("فشل بعد جميع المحاولات:", error.message);
}
// متقدم: إعادة المحاولة مع شروط مخصصة
async function retryWithCondition(fn, options = {}) {
const {
maxRetries = 3,
baseDelay = 1000,
shouldRetry = () => true,
onRetry = () => {}
} = options;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries || !shouldRetry(error, attempt)) {
throw error;
}
const delay = baseDelay * Math.pow(2, attempt - 1);
onRetry(error, attempt, delay);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// الاستخدام مع شروط إعادة محاولة مخصصة
const data = await retryWithCondition(
() => fetchUnreliableAPI(),
{
maxRetries: 5,
baseDelay: 500,
shouldRetry: (error, attempt) => {
// أعد المحاولة فقط على أخطاء الشبكة، وليس أخطاء العميل 4xx
return !error.message.includes("HTTP 4");
},
onRetry: (error, attempt, delay) => {
console.log(`إعادة المحاولة ${attempt}: ${error.message} (انتظار ${delay}ms)`);
}
}
);
تحذير: كن حذراً مع منطق إعادة المحاولة على استدعاءات API التي تعدل البيانات (POST، PUT، DELETE). إعادة محاولة هذه العمليات يمكن أن تؤدي إلى إجراءات مكررة أو حالة غير متسقة.
معالجة انتهاء المهلة
أضف مهلات للعمليات غير المتزامنة لمنع التعليق:
// إنشاء Promise انتهاء المهلة
function timeout(ms, message = "انتهت مهلة العملية") {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error(message)), ms);
});
}
// سباق بين العملية وانتهاء المهلة
async function withTimeout(promise, ms) {
return Promise.race([
promise,
timeout(ms)
]);
}
// الاستخدام
try {
const data = await withTimeout(
fetch("/api/slow-endpoint").then(r => r.json()),
5000
);
console.log("نجح:", data);
} catch (error) {
if (error.message.includes("انتهت مهلة")) {
console.error("استغرق الطلب وقتاً طويلاً");
} else {
console.error("فشل الطلب:", error);
}
}
// متقدم: انتهاء المهلة مع AbortController
async function fetchWithTimeout(url, ms = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), ms);
try {
const response = await fetch(url, {
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === "AbortError") {
throw new Error(`انتهت مهلة الطلب بعد ${ms}ms`);
}
throw error;
}
}
نمط من العالم الحقيقي: معالجة قائمة الانتظار
// معالج قائمة انتظار غير متزامنة
class AsyncQueue {
constructor(concurrency = 1) {
this.concurrency = concurrency;
this.running = 0;
this.queue = [];
}
async add(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject });
this.process();
});
}
async process() {
if (this.running >= this.concurrency || this.queue.length === 0) {
return;
}
this.running++;
const { task, resolve, reject } = this.queue.shift();
try {
const result = await task();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.running--;
this.process();
}
}
async waitForAll() {
while (this.running > 0 || this.queue.length > 0) {
await new Promise(resolve => setTimeout(resolve, 10));
}
}
}
// الاستخدام
const queue = new AsyncQueue(3); // 3 عمليات متزامنة كحد أقصى
const tasks = [
() => fetch("/api/user/1").then(r => r.json()),
() => fetch("/api/user/2").then(r => r.json()),
() => fetch("/api/user/3").then(r => r.json()),
() => fetch("/api/user/4").then(r => r.json()),
() => fetch("/api/user/5").then(r => r.json())
];
// أضف جميع المهام إلى قائمة الانتظار
const results = await Promise.all(
tasks.map(task => queue.add(task))
);
console.log("تم جلب جميع المستخدمين:", results);
تمرين تطبيقي:
المهمة: أنشئ دالة تجلب البيانات من عدة عناوين URL مع تحديد المعدل ومنطق إعادة المحاولة ومعالجة انتهاء المهلة.
// مهمتك: تنفيذ هذه الدالة
async function fetchMultipleWithControls(urls, options = {}) {
// الخيارات:
// - maxConcurrent: أقصى عدد من الطلبات المتوازية
// - timeout: مهلة الطلب بالملي ثانية
// - maxRetries: الحد الأقصى لمحاولات إعادة المحاولة
// - retryDelay: التأخير الأساسي بين المحاولات
// أرجع مصفوفة من النتائج بتنسيق:
// { url, success: true, data } أو { url, success: false, error }
}
الحل:
async function fetchMultipleWithControls(urls, options = {}) {
const {
maxConcurrent = 3,
timeout: timeoutMs = 5000,
maxRetries = 2,
retryDelay = 1000
} = options;
async function fetchWithRetry(url) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
const response = await fetch(url, {
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
return { url, success: true, data };
} catch (error) {
if (attempt === maxRetries) {
return {
url,
success: false,
error: error.message
};
}
const delay = retryDelay * attempt;
console.log(`${url}: إعادة المحاولة ${attempt} بعد ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// معالجة مع حد التزامن
const results = [];
const executing = [];
for (const url of urls) {
const promise = fetchWithRetry(url).then(result => {
executing.splice(executing.indexOf(promise), 1);
return result;
});
results.push(promise);
executing.push(promise);
if (executing.length >= maxConcurrent) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
// الاستخدام
const urls = [
"https://api.example.com/user/1",
"https://api.example.com/user/2",
"https://api.example.com/user/3",
"https://api.example.com/user/4",
"https://api.example.com/user/5"
];
const results = await fetchMultipleWithControls(urls, {
maxConcurrent: 2,
timeout: 3000,
maxRetries: 3,
retryDelay: 500
});
results.forEach(result => {
if (result.success) {
console.log(`✓ ${result.url}:`, result.data);
} else {
console.log(`✗ ${result.url}:`, result.error);
}
});
الملخص
في هذا الدرس، تعلمت:
- for await...of للتكرار غير المتزامن المتسلسل
- المولدات غير المتزامنة لإنتاج Promises
- استراتيجيات التنفيذ المتوازي مقابل المتسلسل
- التنظيم لتحديد العمليات المتزامنة
- Debouncing لتقليل الاستدعاءات غير المتزامنة المفرطة
- أنماط إعادة المحاولة مع تأخير متزايد
- معالجة انتهاء المهلة لمنع العمليات المعلقة
- معالجة قائمة الانتظار للتزامن المتحكم فيه
التالي: في الدرس التالي، سنتعمق في حلقة أحداث JavaScript لفهم كيف يعمل الكود غير المتزامن حقاً من الداخل!