Node.js: Promise و async/await
آخر تحديث: 2026-08-26
1. ما ستتعلمه
- الأساليب الأساسية وحالات الاستخدام لواجهة برمجة التطبيقات fs.promises
- الاختلافات السلوكية والخيارات بين Promise.all و race و allSettled و any
- معالجة الأخطاء باستخدام غير متزامن/await و try/catch
- التحكم في التزامن: الحد من عدد المهام غير المتزامنة التي تعمل في وقت واحد
- حالات استخدام التنفيذ التسلسلي، والتنفيذ المتوازي، والتزامن المحدود
- أفضل الممارسات لاستدعاءات الـ«Promise» المتسلسلة
- تقوم الأداة المساعدة util.promisify بتحويل الكود المكتوب بأسلوب «الاستدعاء المرتد» إلى «الوعود» (Promises)
2. القصة: تنزيل تشارلي للبيانات الضخمة
كان تشارلي مسؤولاً عن تنزيل بيانات التكوين من 100 واجهة برمجة تطبيقات خارجية. في البداية، استخدم for لإرسال الطلبات واحدًا تلو الآخر في حلقة؛ وكان كل طلب يستغرق حوالي 30 ثانية، لذا استغرق الأمر 50 دقيقة لإكمال الـ 100 طلب. لاحقًا، تحول إلى استخدام Promise.all لإرسال الطلبات بشكل متوازٍ، مما خفض الوقت الإجمالي إلى 30 ثانية—لكن الخادم أرجع على الفور 429 Too Many Requests. أدرك تشارلي أنه بحاجة إلى تحديد عدد الطلبات المتزامنة، فحددها في النهاية بـ 5 طلبات، مما خفض الوقت الإجمالي إلى حوالي 10 دقائق — وهو حل كان فعالاً في الوقت نفسه ولم يؤدي إلى تفعيل آلية تحديد معدل الطلبات.
تسلط هذه القصة الضوء على المفاضلة الأساسية في البرمجة غير المتزامنة: السرعة مقابل الموارد. وتُعد الوعود (Promises) وasync/await الأدوات المستخدمة لإدارة هذا التوازن.
3. واجهة برمجة تطبيقات fs.promises
ابتداءً من الإصدار 10، يوفر Node.js fs.promises، الذي يحول عمليات نظام الملفات إلى دالات تُرجع وعودًا (Promises)، مما يضع حدًا لـ «جحيم الاستدعاءات المرتدة» (callback hell).
(1) دالات الاستدعاء في fs مقابل fs.promises مقابل promisify
| ميزة | دالات الاستدعاء المرتدة لنظام الملفات | fs.promises | util.promisify(fs.xxx) |
|---|---|---|---|
| قيمة الإرجاع | void؛ يتم تمرير النتيجة عبر دالة استدعاء | Promise | Promise |
| معالجة الأخطاء | المعلمة الأولى لدالة الاستدعاء، err |
try/catch أو .catch() |
try/catch أو .catch() |
| أسلوب الكتابة | الدوال الاستدعائية المتداخلة | الأسلوب المسطح للعمليات غير المتزامنة/await | الأسلوب المسطح للعمليات غير المتزامنة/await |
| الإصدارات المتاحة | جميع الإصدارات | الإصدار 10 وما فوق | الإصدار 8 وما فوق |
| الاستخدام النموذجي | fs.readFile(path, (err, data) => {}) |
await fs.promises.readFile(path) |
const readFile = promisify(fs.readFile); await readFile(path) |
▶ مثال: قراءة الملفات وكتابتها باستخدام fs.promises
const fs = require('fs').promises;
async function readAndWrite() {
try {
const data = await fs.readFile('input.txt', 'utf8');
const upper = data.toUpperCase();
await fs.writeFile('output.txt', upper);
console.log('Done');
} catch (err) {
console.error('Error:', err.message);
}
}
readAndWrite();
▶ مثال: عمليات الدليل باستخدام fs.promises
const fs = require('fs').promises;
غير متزامن الدالة listFiles(dir) {
try {
await fs.mkdir(dir, { recursive: true });
const files = await fs.readdir(dir);
for (const file of files) {
const stat = await fs.stat(`${dir}/${file}`);
console.log(`${file} - ${stat.size} bytes`);
}
} catch (err) {
console.error(err.message);
}
}
listFiles('./my-dir');
▶ مثال: تقوم الدالة util.promisify بتحويل دوال الاستدعاء
const fs = require('fs');
const { promisify } = require('util');
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
async function copy() {
const data = await readFile('source.txt', 'utf8');
await writeFile('dest.txt', data);
console.log('Copied');
}
copy();
4. طرق تسلسل الوعود
تحدد أربع طرق ثابتة كيفية تفاعل عدة وعود (Promises) مع بعضها البعض؛ وقد يؤدي اختيار تركيبة خاطئة من هذه الطرق إلى نتائج مختلفة تمامًا.
(1) مقارنة بين أربع طرق للجمع
| الطريقة | عند نجاح جميع العمليات | عند حدوث فشل | القيمة المرجعة | الاستخدامات الشائعة |
|---|---|---|---|---|
Promise.all |
إرجاع مصفوفة النتائج بالكامل | الرفض عند أول فشل؛ رفض المجموعة بأكملها | مصفوفة النتائج | يجب إكمال جميع المهام |
Promise.race |
إرجاع أول نتيجة مكتملة | الرفض عند أول فشل | قيمة واحدة | التحكم في مهلة الانتظار، أسرع استجابة |
Promise.allSettled |
إرجاع جميع النتائج | عدم استبعاد أي نتائج؛ تضمين رسائل الفشل | {status, value/reason}[] |
إرجاع جميع النتائج، بغض النظر عن النجاح أو الفشل |
Promise.any |
إرجاع أول نتيجة ناجحة | الرفض فقط في حالة فشل جميع المحاولات (AggregateError) | قيمة فردية | تضارب بين مصادر متعددة، اختيار أسرع نتيجة ناجحة |
▶ مثال: Promise.all — لا يتم تنفيذها إلا إذا نجحت جميع الوعود
async function fetchAll() {
const urls = [
'https://api.example.com/a',
'https://api.example.com/b',
'https://api.example.com/c',
];
try {
const results = await Promise.all(
urls.map(url => fetch(url).then(r => r.json()))
);
console.log('All succeeded:', results.length);
} catch (err) {
console.error('One failed:', err.message);
}
}
▶ مثال: Promise.allSettled — لا يتم رفضها أبدًا
غير متزامن الدالة fetchAllSettled() {
const tasks = [
Promise.resolve({ id: 1 }),
Promise.reject(new Error('Server down')),
Promise.resolve({ id: 3 }),
];
const results = await Promise.allSettled(tasks);
const succeeded = results.filter(r => r.status === 'fulfilled');
const failed = results.filter(r => r.status === 'rejected');
console.log(`Succeeded: ${succeeded.length}, Failed: ${failed.length}`);
failed.forEach(r => console.error('Reason:', r.reason.message));
}
▶ مثال: Promise.race — التحكم في مهلة الانتظار
function fetchWithTimeout(url, ms) {
const fetchTask = fetch(url).then(r => r.json());
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
);
return Promise.race([fetchTask, timeout]);
}
async function demo() {
try {
const data = await fetchWithTimeout('https://api.example.com/slow', 3000);
console.log(data);
} catch (err) {
console.error(err.message);
}
}
▶ مثال: Promise.any — استرداد نتيجة ناجحة من مصادر متعددة في حالة التنافس
async function fastestMirror() {
const mirrors = [
fetch('https://mirror1.example.com/data').then(r => r.json()),
fetch('https://mirror2.example.com/data').then(r => r.json()),
fetch('https://mirror3.example.com/data').then(r => r.json()),
];
try {
const result = await Promise.any(mirrors);
console.log('Fastest response:', result);
} catch (err) {
console.error('All mirrors failed:', err.errors.length);
}
}
▶ مثال:(2) Mermaid: مقارنة تنفيذ طريقة تسلسل الوعود
flowchart TB
subgraph all["Promise.all"]
A1["Task A ✅"] --- A2["Task B ✅"] --- A3["Task C ✅"]
AR["→ [A, B, C] ✅"]
end
subgraph race["Promise.race"]
R1["Task A ⏱ 1s"] --- R2["Task B ⏱ 3s"] --- R3["Task C ⏱ 2s"]
RR["→ A ✅ (Fastest)"]
end
subgraph settled["Promise.allSettled"]
S1["Task A ✅"] --- S2["Task B ❌"] --- S3["Task C ✅"]
SR["→ [{fulfilled:A}, {rejected:B}, {fulfilled:C}] ✅"]
end
subgraph any["Promise.any"]
N1["Task A ❌"] --- N2["Task B ✅ ⏱ 2s"] --- N3["Task C ✅ ⏱ 3s"]
NR["→ B ✅ (Success as Quickly as Possible)"]
end
all --> AR
race --> RR
settled --> SR
any --> NR
style AR fill:#c8e6c9
style RR fill:#c8e6c9
style SR fill:#fff9c4
style NR fill:#c8e6c9
5. معالجة الأخطاء باستخدام async/await
تجعل ميزة async/await الكود غير المتزامن يبدو وكأنه كود متزامن، وتتعامل مع الأخطاء بنفس الطريقة try/catch—ولكن هناك بعض العقبات.
(1) مقارنة بين نماذج معالجة الأخطاء
| النمط | التنفيذ | المزايا | العيوب |
|---|---|---|---|
| رد الاتصال | if (err) { handle } |
بسيط وبديهي | تداخل عميق، يسهل إغفاله |
| Promise.catch() | promise.then().catch() |
متسلسل، قابل لإعادة الاستخدام | لا يزال معقدًا عند التداخل |
| async/await + try/catch | try { await } catch {} |
أسلوب متزامن، سهولة قراءة جيدة | يجب تغليف كل await |
| دالة غلاف | const [err, data] = await to(promise) |
بدون try/catch، موجزة | تتطلب استيراد دوال مساعدة |
▶ مثال: تغليف دالة غير متزامنة باستخدام try/catch
const fs = require('fs').promises;
غير متزامن الدالة safeReadFile(path) {
try {
const data = await fs.readFile(path, 'utf8');
return { ok: true, data };
} catch (err) {
return { ok: false, error: err.message };
}
}
غير متزامن الدالة main() {
const result = await safeReadFile('missing.txt');
if (!result.ok) {
console.error('Failed:', result.error);
return;
}
console.log('Content:', result.data);
}
main();
▶ مثال: دالة معالجة الأخطاء بدون استخدام try/catch
function to(promise) {
return promise
.then(data => [null, data])
.catch(err => [err, null]);
}
async function main() {
const fs = require('fs').promises;
const [err, data] = await to(fs.readFile('config.json', 'utf8'));
if (err) {
console.error('Read failed:', err.message);
return;
}
console.log('Config:', data);
}
main();
6. التحكم في التزامن
(1) التزامن التسلسلي مقابل التزامن المتوازي مقابل التزامن المقيد
| طريقة التنفيذ | الوقت الإجمالي (N مهام، تستغرق كل منها وقتًا يساوي T) | المزايا | العيوب | السيناريوهات المناسبة |
|---|---|---|---|---|
| التنفيذ التسلسلي | N × T | بسيط، وموفر للموارد | بطيء | المهام ذات التبعيات |
| متوازي تمامًا | ≈ T | الأسرع | استخدام ذروي مرتفع للموارد؛ قد يتم تقييد السرعة | عدد قليل من المهام المستقلة |
| محدودة من حيث التزامن | ≈ N/عدد عمليات التزامن × T | تحقق التوازن بين السرعة والموارد | أكثر تعقيدًا قليلاً في التنفيذ | عدد كبير من المهام المستقلة، وتحديد معدل استخدام واجهة برمجة التطبيقات |
▶ مثال: التنفيذ التسلسلي
const fs = require('fs').promises;
async function sequential() {
const files = ['a.txt', 'b.txt', 'c.txt'];
const results = [];
for (const file of files) {
const data = await fs.readFile(file, 'utf8');
results.push(data);
}
console.log('Results:', results.length);
}
▶ مثال: دالة التحكم في التزامن المقيدة
غير متزامن الدالة limitConcurrency(tasks, limit) {
const results = [];
const executing = new Set();
for (const task of tasks) {
const p = task().then(result => {
executing.حذف(p);
return result;
});
executing.add(p);
results.push(p);
if (executing.size >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
▶ مثال: استخدام واجهة برمجة التطبيقات (API) الخاصة بعدد التنزيلات المتزامنة المحدود
async function fetchApi(url) {
const res = await fetch(url);
return res.json();
}
async function batchFetch() {
const urls = Array.from({ length: 100 }, (_, i) =>
`https://api.example.com/item/${i + 1}`
);
const tasks = urls.map(url => () => fetchApi(url));
const results = await limitConcurrency(tasks, 5);
console.log(`Fetched ${results.length} items`);
}
7. أفضل الممارسات لتسلسل الوعود
(1) قواعد المكالمات المتسلسلة
| القاعدة | الوصف | المثال المضاد |
|---|---|---|
| إرجاع Promise دائمًا | ضمان التسلسل | プロミス.then(() => { doSomething() }) لا إرجاع |
| تعامل دائمًا مع الأخطاء | أضف .catch() في النهاية | يؤدي استخدام كتلة متسلسلة بدون .catch() إلى حدوث أخطاء غير معالجة |
| تجنب التداخل | تسوية سلاسل .then() | .then(() => { return p.then(...) }) |
| استبدال السلاسل الطويلة بـ غير متزامن/await | استبدال أكثر من 3 استدعاءات لـ .then() بـ await | أكثر من 5 مستويات من استدعاءات .then() المتداخلة |
| ملاحظة: سيتم التقاط استثناء «throw» في .then() | بواسطة .catch() التالي | لأنني اعتقدت أن الاستثناء لن يقطع سير العملية |
▶ مثال: استدعاء سلسلة مسطحة
const fs = require('fs').promises;
function processFile(path) {
return fs.readFile(path, 'utf8')
.then(data => data.trim())
.then(data => data.toUpperCase())
.then(data => fs.writeFile('output.txt', data))
.then(() => console.log('Saved'))
.catch(err => console.error('Error:', err.message));
}
processFile('input.txt');
8. مثال شامل: أداة لمعالجة الملفات دفعةً واحدةً مع تقييد التزامن
إنشاء أداة: قراءة دليل → تحديد عدد عمليات الملفات المتزامنة بثلاث عمليات → جمع النتائج → إخراج الإحصائيات.
const fs = require('fs').promises;
const path = require('path');
غير متزامن الدالة processFile(filePath) {
const stat = await fs.stat(filePath);
const content = await fs.readFile(filePath, 'utf8');
const lines = content.split('\n').length;
const words = content.split(/\s+/).filter(Boolean).length;
return {
file: path.basename(filePath),
size: stat.size,
lines,
words,
};
}
غير متزامن الدالة limitConcurrency(tasks, limit) {
const results = [];
const executing = new Set();
for (const task of tasks) {
const p = task().then(result => {
executing.حذف(p);
return result;
});
executing.add(p);
results.push(p);
if (executing.size >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
غير متزامن الدالة batchProcessDir(dirPath, concurrency = 3) {
console.log(`Scanning directory: ${dirPath}`);
const files = await fs.readdir(dirPath);
const filePaths = files
.filter(f => f.endsWith('.txt') || f.endsWith('.md') || f.endsWith('.json'))
.map(f => path.الربط(dirPath, f));
if (filePaths.length === 0) {
console.log('No matching files found.');
return;
}
const tasks = filePaths.map(fp => () => processFile(fp));
const results = await limitConcurrency(tasks, concurrency);
console.log('\n--- File Statistics ---');
console.log('File'.padEnd(20) + 'Size'.padEnd(10) + 'Lines'.padEnd(8) + 'Words');
console.log('-'.repeat(46));
let totalLines = 0;
let totalWords = 0;
let totalSize = 0;
for (const r of results) {
console.log(
r.file.padEnd(20) +
String(r.size).padEnd(10) +
String(r.lines).padEnd(8) +
String(r.words)
);
totalLines += r.lines;
totalWords += r.words;
totalSize += r.size;
}
console.log('-'.repeat(46));
console.log(
'TOTAL'.padEnd(20) +
String(totalSize).padEnd(10) +
String(totalLines).padEnd(8) +
String(totalWords)
);
console.log(`\nProcessed ${results.length} files (concurrency: ${concurrency})`);
}
batchProcessDir('./data', 3).catch(err => console.error('Fatal:', err.message));
الأداء:
Scanning directory: ./data
--- File Statistics ---
File Size Lines Words
----------------------------------------------
config.json 256 12 42
readme.md 1024 45 312
notes.txt 512 28 178
----------------------------------------------
TOTAL 1792 85 532
Processed 3 files (concurrency: 3)
❓ أسئلة شائعة
Promise.all؟Promise.allSettled عندما تحتاج إلى جميع النتائج.await التي تتم في وقت واحد؟limitConcurrency تستخدم Set لتتبع الوعود (Promises) النشطة وPromise.race للتحكم في عدد العمليات المتزامنة؛ وفي بيئات الإنتاج، يمكنك أيضًا استخدام مكتبة p-limit.await فقط داخل دوال async؟await على المستوى الأعلى، مما يسمح باستخدام await مباشرةً على المستوى الأعلى من وحدة ES، ولكن في وحدات CommonJS، لا يزال من الضروري تغليفها داخل دالة async.util.promisify تحويل جميع دوال الاستدعاء المرتد؟(err, result) => {}. أما دوال الاستدعاء المرتد المخصصة ذات المعلمات المتعددة فيجب تغليفها يدويًّا.Promise.allSettled النتائج بنفس الترتيب الذي جاءت به المدخلات؟async؟async دائمًا وعدًا (Promise). وحتى إذا رجعت قيمة عادية، يتم تغليفها تلقائيًا في Promise.resolve(value).📖 ملخص
- المفاهيم الأساسية وكيفية تطبيقها
- المقال: المفاهيم الأساسية واستخدامات أداة «Charlie's Batch Data Download»
- المفاهيم الأساسية واستخدام واجهة برمجة التطبيقات fs.promises
- المفاهيم الأساسية واستخدامات أساليب تسلسل الوعود (Promise Chaining)
- المفاهيم الأساسية واستخدامات معالجة الأخطاء باستخدام غير متزامن/await
- المفاهيم الأساسية للتحكم في التزامن وكيفية استخدامه
- المفاهيم الأساسية وأفضل الممارسات لتسلسل الوعود
- مثال شامل: المفاهيم الأساسية واستخدامات أداة معالجة الملفات دفعةً واحدةً ذات التزامن المحدود
📝 تمارين
- أكمل جميع أمثلة الأكواد الواردة في هذا الدرس وتأكد من أن كل منها يعمل بشكل صحيح.
- قم بتعديل المثال الشامل وأضف الإضافات الخاصة بك
- راجع الوثائق الرسمية، وحدد واجهة برمجة تطبيقات (API) واحدة أو اثنتين لم يتم تناولهما في هذا الدرس، واكتب كود اختبار لهما.
- التأمل: كيف ستطبق ما تعلمته في هذا الدرس على مشروع في الواقع العملي؟
- حاول أن تجمع بين ما تعلمته في هذا الدرس والمواد التي درستها في الدروس السابقة لـ build مشروعًا صغيرًا.