إتقان JavaScript أنماط غير متزامنة: من مكالمات العودة إلى التزامن
١١ نوفمبر ٢٠٢٥
TL;DR
- JavaScript’s التطور غير المتزامن: كولباك → وعود → async/await → التزامن المنظم.
- حلقة الأحداث وطابور المهام الصغيرة تُعزز الأداء غير المعيق.
- إتقان البرمجة غير المتزامنة = التحكم في الإلغاء، التنسيق، والصحة.
- أنماط البرمجة غير المتزامنة الحديثة تُحسّن القابلية للتوسع والموثوقية لكنها تتطلب معالجة أخطاء منهجية.
- اختيار الأداة المناسبة للبرمجة غير المتزامنة هو المفتاح لكتابة كود إنتاجي وقابل للصيانة.
ما ستتعلمه
- كيف تعمل حلقة الأحداث ونموذج التزامن في JavaScript فعليًا.
- التطور والتوازنات بين كولباك، الوعود، وasync/await.
- كيف تدير التزامن، الإلغاء، وانتشار الأخطاء بأمان.
- كيف تطبق أنماط البرمجة غير المتزامنة على الأنظمة الواقعية (واجهات برمجة التطبيقات، التدفقات، والعاملين).
- كيف تختبر، تراقب، وتقوم بتوسيع التطبيقات غير المتزامنة.
المتطلبات الأساسية
قبل الغوص في الموضوع، تأكد من أنك مرتاح مع:
- مفاهيم JavaScript الأساسية (الدوال، الإغلاق، الكائنات)
- بنية ES6+ (دوال السهم، التفكيك)
- واجهات برمجة التطبيقات الأساسية لـ Node.js أو المتصفح
إذا كنت قد كتبت من قبل async function أو استخدمت fetch(), فأنت مستعد.
مقدمة: لماذا البرمجة غير المتزامنة مهمة
JavaScript يعمل على خيط واحد—مكدس استدعاء واحد، حلقة أحداث واحدة. ومع ذلك، تتعامل التطبيقات الحديثة مع مئات العمليات المتزامنة: طلبات الشبكة، قراءة الملفات، تفاعلات المستخدم، والمهام الخلفية. بدون البرمجة غير المتزامنة، ستوقف كل عملية إدخال/إخراج الخيط، مما يؤدي إلى تجمد الواجهة أو إيقاف طلبات الخلفية.
البرمجة غير المتزامنة هي ما تجعل JavaScript متجاوبة. هكذا تستطيع تطبيقات الويب جلب البيانات أثناء تحديث DOM، أو كيف يمكن لـ Node.js API خدمة آلاف الطلبات المتزامنة دون إنشاء خيوط جديدة1.
لكن البرمجة غير المتزامنة ليست مجرد بناء جملي—بل هي عن التحكم: معرفة متى تبدأ المهام، تنتهي، وتفشل. لإتقان JavaScript غير المتزامن، نحتاج إلى فهم كيفية تطور أنماطه.
تطور البرمجة غير المتزامنة في JavaScript
| النمط | العصر | أسلوب الصياغة | معالجة الأخطاء | الوضوح | الاستخدام النموذجي |
|---|---|---|---|---|---|
| كولباك | ES3 | دوال متداخلة | يدوي | ضعيف | واجهات Node.js القديمة |
| الوعود | ES6 | متسلسل .then() |
.catch() |
متوسط | طلبات HTTP، واجهات برمجة التطبيقات غير المتزامنة |
| async/await | ES2017 | async/await |
try/catch |
ممتاز | تدفق التحكم غير المتزامن الحديث |
| التدفقات، العاملين، المراقبين | ES2020+ | موجه بالأحداث | سياقي | متغير | أنظمة عالية الإنتاجية أو تفاعلية |
كل خطوة حسّنت القابلية للتركيب وتجربة المطور مع إدخال تنازلات جديدة في الأداء ومعالجة الأخطاء.
فهم حلقة الأحداث
قبل الغوص في الأنماط، دعونا نصور حلقة الأحداث، القلب النابض لنموذج البرمجة غير المتزامن في JavaScript.
flowchart TD
A[Call Stack] -->|Empty| B[Event Loop]
B --> C[Callback Queue]
B --> D[Microtask Queue]
D -->|Higher Priority| A
C -->|Executed Next| A
- مكدس الاستدعاءات: ينفذ الكود المتزامن.
- طابور المهام الصغيرة: يحتوي على كولباك الوعود واستمراريات async/await.
- طابور الكولباك (طابور المهام الكبيرة): يحتوي على إدخال/إخراج، مؤقتات، وأحداث DOM.
تتحقق حلقة الأحداث باستمرار مما إذا كان مكدس الاستدعاءات فارغًا. عندما يكون كذلك، تعالج أولاً جميع المهام الصغيرة قبل الانتقال إلى المهام الكبيرة2. هذا يفسر لماذا يعمل وعده مُحَلَّل قبل كولباك setTimeout() في نفس التكتكة.
النمط 1: كولباك
كيف يعمل
كانت الكولباك أول آلية غير متزامنة في JavaScript: تمرير دالة لتنفيذها عند اكتمال العملية.
import fs from 'fs';
fs.readFile('data.txt', 'utf8', (err, data) => {
if (err) return console.error('Error:', err);
console.log('File contents:', data);
});
المشكلة: جحيم الكولباك
عندما تعتمد المهام على بعضها، تصبح الكولباك متداخلة بعمق:
getUser(id, (err, user) => {
if (err) return handleError(err);
getPosts(user.id, (err, posts) => {
if (err) return handleError(err);
getComments(posts[0].id, (err, comments) => {
console.log(comments);
});
});
});
هذا «هرم الشر» يجعل تصحيح الأخطاء واختبار النظام ومعالجة الأخطاء صعبًا.
متى تستخدم
- مهام غير متزامنة بسيطة وفريدة.
- واجهات برمجة Node.js القديمة التي لم يتم تحويلها إلى وعود.
متى لا تستخدم
- التدفقات المعقدة أو السلاسل المتزامنة المعتمدة.
- قواعد الكود التي تتطلب قابلية صيانة عالية.
النمط الثاني: الوعود
الوعود، التي تم تقديمها في ES6، حلّت جحيم الكولباك. الوعد يمثل قيمة قد تكون متاحة الآن، لاحقًا، أو أبدًا.
fetch('https://API.example.com/data')
.then(res => res.json())
.then(data => console.log('Data:', data))
.catch(err => console.error('Error:', err));
الميزات الرئيسية
- قابلة للربط والتركيب.
- تدفقات غير متزامنة مسطحة.
- نشر الأخطاء المدمج.
الخطأ الشائع: الرفضات غير المعالجة
إذا نسيت .catch()، يمكن أن تؤدي الرفضات غير المعالجة إلى إيقاف عمليات Node.js في الإصدارات الحديثة3.
الحل: قم دائمًا بإرفاق .catch() أو استخدام معالج عالمي:
process.on('unhandledRejection', err => {
console.error('Unhandled rejection:', err);
});
ملاحظة الأداء
الوعود تُدخل تحميلًا من المهام الدقيقة. بالنسبة لأحمال العمل المحدودة بـI/O، فهو ضئيل؛而对于 مهام المعالجة الثقيلة، يمكن أن يضيف تأخيرًا طفيفًا.
النمط الثالث: Async/Await
تم تقديمها في ES2017، جعل async/await الكود غير المتزامن يبدو متزامنًا—دون حظر.
async function fetchUserData(id) {
try {
const res = await fetch(`/API/users/${id}`);
return await res.json();
} catch (err) {
console.error('Failed to fetch user:', err);
}
}
لماذا هو أفضل
- تدفق تحكم خطي وسهل القراءة.
- معالجة الأخطاء المدمجة مع
try/catch. - مثالي للمنطق غير المتزامن المتسلسل.
متى تستخدم
- عندما تعتمد خطوات غير متزامنة على النتائج السابقة.
- لقواعد كود واضحة وقابلة للصيانة.
متى لا تستخدم
- لمهام مستقلة يمكن تشغيلها بالتوازي—استخدم
Promise.all()بدلاً من ذلك.
مثال: موازاة المهام غير المتزامنة
قبل (متسلسل):
async function loadData() {
const user = await fetchUser();
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
return { user, posts, comments };
}
بعد (متوازي):
async function loadData() {
const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
const comments = await fetchComments(posts[0].id);
return { user, posts, comments };
}
موازاة المهام المستقلة تقلل عادةً التأخير في السيناريوهات المحدودة بالإدخال/الإخراج5.
أنماط متقدمة: التدفقات، العمال، والمراقبات
التدفقات
تتعامل تدفقات Node.js مع البيانات الكبيرة بكفاءة دون تحميلها كلها في الذاكرة.
import fs from 'fs';
const readStream = fs.createReadStream('bigfile.log');
readStream.on('data', chunk => console.log('Received', chunk.length, 'bytes'));
readStream.on('end', () => console.log('Done'));
التدفقات تُصدر أحداثًا بشكل غير متزامن أثناء تدفق البيانات6.
Web Workers
في المتصفحات، Web Workers تشغل النصوص البرمجية في خيوط خلفية وتتواصل بشكل غير متزامن.
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ task: 'compute' });
worker.onmessage = e => console.log('Result:', e.data);
Observables
انتشرت بواسطة RxJS، المراقبات توفر تدفقات بيانات غير متزامنة على مر الزمن—مثالية للتطبيقات في الوقت الحقيقي.
import { fromEvent } from 'rxjs';
fromEvent(document, 'click').subscribe(event => console.log('Clicked!', event));
متى تستخدم مقابل متى لا تستخدم
| النمط | عند استخدامه | عند تجنبه |
|---|---|---|
| الدوال العودية | واجهات قديمة، تفاعلات غير متزامنة بسيطة | تدفقات معقدة |
| الوعود | منطق غير متزامن متسلسل | تزامن ثقيل |
| غير متزامن/انتظر | منطق غير متزامن متسلسل | حلقات تزامن عالية |
| التدفقات | بيانات كبيرة أو مستمرة | قراءات صغيرة فردية |
| العمال | مهام مقيدة بالمعالج | تحديثات DOM بسيطة |
| المراقبات | في الوقت الحقيقي، قائمة على الأحداث | جلب بيانات ثابتة |
مثال عملي: التزامن غير المتزامن في الإنتاج
تستخدم خدمات Node.js الكبيرة غالبًا غير متزامن/انتظر مع أدوات التحكم في التزامن مثل Promise.allSettled() ومجموعات مخصصة للحفاظ على الإنتاجية دون حظر دورة الأحداث7.
مثال على التزامن المُحكم
async function fetchInBatches(urls, limit = 5) {
const results = [];
while (urls.length) {
const batch = urls.splice(0, limit);
const responses = await Promise.allSettled(batch.map(url => fetch(url)));
results.push(...responses);
}
return results;
}
هذه الطريقة توازن التزامن لتجنب إرهاق واجهات برمجة التطبيقات أو استنفاد الذاكرة.
المزالق الشائعة والحلول
| المزالق | الوصف | الحل |
|---|---|---|
| رفض غير معالج | مفقود .catch() |
يجب دائمًا ربط معالجات الأخطاء |
| حظر حلقة الأحداث | مهام ثقيلة على المعالج | استخدم Workers أو عمليات فرعية |
| استدعاءات متتالية أسينكرون | await داخل الحلقات |
استخدم Promise.all() أو التجميع |
| تسريب الذاكرة | وعود غير محلولة | استخدم فترات انتظار أو رموز إلغاء |
مثال: تجنب استخدام Await في الحلقات
سيء:
for (const id of ids) {
await fetchUser(id); // sequential
}
جيد:
await Promise.all(ids.map(id => fetchUser(id))); // parallel
أنماط معالجة الأخطاء
1. محاولة/التقاط مع الدوال غير المتزامنة
try {
const data = await fetchData();
} catch (err) {
console.error('Error:', err);
}
2. معالجة الرفض العالمي
process.on('unhandledRejection', err => {
console.error('Unhandled rejection:', err);
});
3. التدهور اللطيف
const results = await Promise.allSettled(tasks);
results.forEach(r => {
if (r.status === 'fulfilled') console.log(r.value);
else console.warn('Failed:', r.reason);
});
اختبار الكود غير المتزامن
مثال باستخدام Jest
test('fetches data correctly', async () => {
const data = await fetchData();
expect(data).toHaveProperty('id');
});
نصيحة احترافية: دائماً return أو await للوعود في الاختبارات — وإلا قد ينتهي الاختبار قبل أن يُحل الكود غير المتزامن.
المراقبة والقابلية للملاحظة
يمكن لأخطاء غير المتزامنة أن تختفي بصمت. أضف تسجيلًا منظمًا وتتبعًا.
import pino from 'pino';
const logger = pino();
async function processOrder(orderId) {
logger.info({ orderId }, 'Processing order');
try {
const result = await chargeCustomer(orderId);
logger.info({ orderId, result }, 'Order processed');
} catch (err) {
logger.error({ orderId, err }, 'Order failed');
}
}
استخدم أدوات مثل OpenTelemetry أو Datadog APM لتتبع الفترات غير المتزامنة عبر الأنظمة الموزعة8.
اعتبارات الأمان
- تجنب حالات السباق: استخدم قفلًا أو طوابير عندما تقوم العمليات غير المتزامنة بتعديل الحالة المشتركة.
- تحقق من المدخلات غير المتزامنة: لا تثق أبدًا بالبيانات من المصادر غير المتزامنة (مثل واجهات برمجة التطبيقات أو البث).
- منع هجمات DoS: قم بتحديد التزامن وضبط فترات الانتظار لتجنب استنفاد الموارد.
رؤى حول القابلية للتوسع
تتسع أنماط غير المتزامنة أفقيًا — تتيح مزيدًا من العمليات المتزامنة دون إضافة خيوط. ومع ذلك:
Promise.all()يمكن أن تُثقل واجهات برمجة التطبيقات إذا استُخدمت بدون حدود.- التدفقات تتوسع جيدًا لأنابيب البيانات الثقيلة.
- العمال يوسعون المهام المرتبطة بالمعالج عبر النوى.
تُستخدم هذه الأنماط على نطاق واسع في خدمات Node.js واسعة النطاق لتحقيق تزامن عالٍ وإنتاجية عالية7.
دليل استكشاف الأخطاء وإصلاحها
| الأعراض | السبب المحتمل | الحل |
|---|---|---|
| تجمد التطبيق | كود متزامن يعيق | نقل إلى Worker أو async API |
| تسرب الذاكرة | وعود غير محلولة | استخدم مغلفات تايم آوت |
| API أخطاء حد السرعة | طلبات متزامنة كثيرة جدًا | أضف محدد التزامن |
| سجلات مفقودة | أخطاء async مبتلعة | أضف معالجات رفض عالمية |
الأخطاء الشائعة التي يرتكبها الجميع
- استخدام
awaitداخل الحلقات بشكل غير ضروري. - نسيان التعامل مع الوعود المرفوضة.
- خلط الدعوات العودية والوعود في نفس الدالة.
- الافتراض أن async/await يجعل الكود متوازيًا (لا يفعل ذلك افتراضيًا).
- تجاهل إلغاء العملية ومعالجة التايم آوت.
تحدي جربه بنفسك
اكتب دالة تقوم بما يلي:
- يجلب عدة واجهات برمجة تطبيقات بشكل متزامن.
- يعيد محاولة الطلبات الفاشلة حتى 3 مرات.
- يعيد جميع الاستجابات الناجحة.
تلميح: دمج Promise.allSettled() مع مغلف إعادة المحاولة.
النقاط الرئيسية
إتقان async = التحكم، الوضوح، والتزامن.
- استخدم async/await للقراءة، والوعود للتركيب.
- تعامل دائمًا مع الرفض والتوقيت.
- راقب تدفقات async — الأخطاء الصامتة يمكن أن تُضعف الأداء.
- قم بالتوسع بأمان مع تحكم في التزامن.
أسئلة شائعة
س1. هل يجعل async/await JavaScript متعدد الخيوط؟
لا. يجعل I/O غير معيق أسهل في التعبير. JavaScript لا يزال يعمل على خيط واحد لكل حلقة حدث2.
س2. ما الفرق بين المهام الصغرى والمهام الكبرى؟
المهام الصغرى (الوعود) تُنفَّذ قبل المهام الكبرى (الموقِّتات، I/O). هذا يؤثر على ترتيب التنفيذ.
س3. هل الدوال async أسرع من الوعود؟
هي سكر بنائي فوق الوعود. الأداء مشابه تقريبًا.
س4. كيف ألغي عملية async؟
استخدم AbortController (لـ fetch) أو منطق إلغاء مخصص.
س5. هل يمكن خلط الدعوات العودية مع الوعود؟
يمكنك ذلك، لكن من الأفضل تحويل الدعوات العودية إلى وعود باستخدام util.promisify().
الخطوات التالية
- استكشف مكتبات التحكم في التزامن مثل
p-limitأوBottleneck. - تعلم عن Observable (RxJS) للبرمجة التفاعلية async.
- أضف تتبع OpenTelemetry إلى خدمات async الخاصة بك.
إذا أعجبك هذا الاستعراض المعمق، اشترك للبقاء على اطلاع على ممارسات هندسة JavaScript الحديثة.
ملاحظات
-
وثائق Node.js – عن حلقة الحدث https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ ↩
-
وثائق MDN Web – حلقة الحدث https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop ↩ ↩2
-
وثائق Node.js – رفض الوعود غير المعالجة https://nodejs.org/API/process.html#event-unhandledrejection ↩
-
مدونة V8 – JavaScript الوعود والمهام الصغرى https://v8.dev/blog/fast-async ↩
-
Web.dev – تحسين Async JavaScript https://web.dev/promises/ ↩
-
وثائق تدفقات Node.js API https://nodejs.org/API/stream.html ↩
-
مدونة Netflix Tech – توسيع نطاق ميكروسيرفس Node.js https://netflixtechblog.com/scaling-node-js-microservices-at-netflix-7a5e1d5b7e5e ↩ ↩2
-
OpenTelemetry JavaScript الوثائق https://opentelemetry.io/docs/instrumentation/js/ ↩