فهم البرمجة غير المتزامنة
البرمجة غير المتزامنة أساسية في Node.js. تسمح لـ Node.js بالتعامل مع عمليات متعددة بشكل متزامن دون حظر خيط التنفيذ، مما يجعلها مثالية للتطبيقات كثيفة الإدخال/الإخراج.
مفهوم رئيسي: Node.js أحادي الخيط لكنه يستخدم العمليات غير المتزامنة للتعامل مع المهام المتزامنة بكفاءة من خلال حلقة الأحداث.
حلقة الأحداث
حلقة الأحداث هي قلب البنية غير المتزامنة لـ Node.js. تسمح لـ Node.js بتنفيذ عمليات غير محظورة على الرغم من أن JavaScript أحادي الخيط.
كيف تعمل حلقة الأحداث
console.log('Start');
setTimeout(() => {
console.log('Timeout callback');
}, 0);
Promise.resolve().then(() => {
console.log('Promise callback');
});
console.log('End');
/* ترتيب الناتج:
Start
End
Promise callback
Timeout callback
*/
شرح ترتيب التنفيذ:
- الكود المتزامن يعمل أولاً: "Start" و "End"
- المهام الصغيرة (Promises) تعمل بعد ذلك: "Promise callback"
- المهام الكبيرة (setTimeout، setInterval) تعمل أخيرًا: "Timeout callback"
نصيحة: للوعود أولوية أعلى من setTimeout/setInterval في حلقة الأحداث. تُنفذ في قائمة انتظار المهام الصغيرة قبل المهام الكبيرة.
ردود النداء (Callbacks): النهج التقليدي
ردود النداء هي دوال تُمرر كمعاملات لتنفيذها لاحقًا عند اكتمال العملية:
const fs = require('fs');
// قراءة ملف مع رد نداء
fs.readFile('data.txt', 'utf8', (error, data) => {
if (error) {
console.error('Error reading file:', error);
return;
}
console.log('File contents:', data);
});
// عمليات متعددة
function fetchUser(id, callback) {
setTimeout(() => {
const user = { id, name: 'John Doe' };
callback(null, user);
}, 1000);
}
fetchUser(1, (error, user) => {
if (error) {
console.error('Error:', error);
return;
}
console.log('User:', user);
});
جحيم رد النداء (هرم الموت)
يمكن أن تصبح ردود النداء المتداخلة صعبة القراءة والصيانة بسرعة:
// سيئ: مثال على جحيم رد النداء
fs.readFile('user.json', 'utf8', (err, userData) => {
if (err) return console.error(err);
const user = JSON.parse(userData);
fs.readFile(`posts/${user.id}.json`, 'utf8', (err, postsData) => {
if (err) return console.error(err);
const posts = JSON.parse(postsData);
fs.readFile(`comments/${posts[0].id}.json`, 'utf8', (err, commentsData) => {
if (err) return console.error(err);
const comments = JSON.parse(commentsData);
console.log(comments);
// يستمر التداخل بشكل أعمق وأعمق...
});
});
});
المشكلة: جحيم رد النداء يؤدي إلى كود يصعب قراءته وتصحيحه وصيانته. الوعود و async/await تحل هذه المشكلة.
الوعود (Promises): نهج أفضل
تمثل الوعود الإكمال النهائي (أو الفشل) لعملية غير متزامنة والقيمة الناتجة عنها.
إنشاء الوعود
// إنشاء وعد
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id < 1) {
reject(new Error('Invalid user ID'));
} else {
resolve({ id, name: 'John Doe', email: 'john@example.com' });
}
}, 1000);
});
}
// استخدام الوعد
fetchUser(1)
.then(user => {
console.log('User:', user);
return user.id;
})
.then(userId => {
console.log('User ID:', userId);
})
.catch(error => {
console.error('Error:', error.message);
})
.finally(() => {
console.log('Operation complete');
});
حالات الوعد
الوعد له ثلاث حالات:
- معلق (Pending): الحالة الأولية، لم يتم الوفاء به أو رفضه
- مكتمل (Fulfilled): اكتملت العملية بنجاح (تم استدعاء resolve)
- مرفوض (Rejected): فشلت العملية (تم استدعاء reject)
ربط الوعود
// أفضل من جحيم رد النداء
function readFilePromise(filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, 'utf8', (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
// وعود مربوطة
readFilePromise('user.json')
.then(userData => {
const user = JSON.parse(userData);
return readFilePromise(`posts/${user.id}.json`);
})
.then(postsData => {
const posts = JSON.parse(postsData);
return readFilePromise(`comments/${posts[0].id}.json`);
})
.then(commentsData => {
const comments = JSON.parse(commentsData);
console.log('Comments:', comments);
})
.catch(error => {
console.error('Error in chain:', error);
});
Async/Await: النهج الحديث
Async/await هو سكر نحوي مبني على الوعود، يجعل الكود غير المتزامن يبدو ويتصرف مثل الكود المتزامن.
Async/Await الأساسي
// دالة async تعيد دائمًا Promise
async function getUser(id) {
// محاكاة عملية غير متزامنة
const user = { id, name: 'John Doe' };
return user; // ملفوف تلقائيًا في Promise.resolve()
}
// استخدام await
async function displayUser() {
try {
const user = await getUser(1);
console.log('User:', user);
} catch (error) {
console.error('Error:', error);
}
}
displayUser();
معالجة الأخطاء باستخدام Try/Catch
const fs = require('fs').promises; // واجهة fs القائمة على الوعود
async function readAndProcessFile(filename) {
try {
// await يوقف التنفيذ حتى يُحل الوعد
const data = await fs.readFile(filename, 'utf8');
const parsed = JSON.parse(data);
console.log('File contents:', parsed);
return parsed;
} catch (error) {
// يلتقط أخطاء fs وأخطاء JSON.parse
console.error('Error:', error.message);
throw error; // إعادة رمي إذا لزم الأمر
} finally {
console.log('File operation complete');
}
}
// استدعاء دالة async
readAndProcessFile('data.json')
.then(result => console.log('Success:', result))
.catch(error => console.error('Failed:', error));
أفضل ممارسة: استخدم دائمًا كتل try/catch مع async/await لمعالجة الأخطاء بشكل صحيح. رفض الوعود غير المعالج يمكن أن يتسبب في تعطل تطبيقك.
التنفيذ المتسلسل مقابل المتوازي
// متسلسل: العمليات تعمل واحدة تلو الأخرى (بطيء)
async function sequential() {
console.time('Sequential');
const user = await fetchUser(1); // انتظر ثانية واحدة
const posts = await fetchPosts(1); // انتظر ثانية واحدة
const comments = await fetchComments(1); // انتظر ثانية واحدة
console.timeEnd('Sequential'); // ~3 ثوانٍ
return { user, posts, comments };
}
// متوازي: العمليات تعمل في نفس الوقت (سريع)
async function parallel() {
console.time('Parallel');
// ابدأ جميع العمليات مرة واحدة
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
]);
console.timeEnd('Parallel'); // ~1 ثانية (أسرع عملية)
return { user, posts, comments };
}
Promise.all() - انتظر الجميع
تنفيذ وعود متعددة بالتوازي والانتظار حتى اكتمال الجميع:
const promise1 = Promise.resolve(3);
const promise2 = new Promise(resolve => setTimeout(() => resolve('foo'), 1000));
const promise3 = fetch('https://api.example.com/data');
// انتظر جميع الوعود
Promise.all([promise1, promise2, promise3])
.then(results => {
console.log('All results:', results);
// [3, 'foo', Response object]
})
.catch(error => {
// إذا رُفض أي وعد، يُستدعى catch
console.error('One promise failed:', error);
});
// الاستخدام مع async/await
async function fetchAllData() {
try {
const [users, posts, comments] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
return { users, posts, comments };
} catch (error) {
console.error('Failed to fetch all data:', error);
}
}
تحذير: Promise.all() يفشل بسرعة. إذا رُفض أي وعد، تفشل العملية بأكملها فورًا.
Promise.allSettled() - انتظر بغض النظر
انتظر حتى تكتمل جميع الوعود بغض النظر عن النجاح أو الفشل:
const promises = [
Promise.resolve('Success 1'),
Promise.reject('Error 1'),
Promise.resolve('Success 2'),
Promise.reject('Error 2')
];
Promise.allSettled(promises)
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Promise ${index} succeeded:`, result.value);
} else {
console.log(`Promise ${index} failed:`, result.reason);
}
});
});
/* الناتج:
Promise 0 succeeded: Success 1
Promise 1 failed: Error 1
Promise 2 succeeded: Success 2
Promise 3 failed: Error 2
*/
Promise.race() - الأول في الاكتمال
يعيد نتيجة أول وعد يكتمل (نجاحًا أو فشلاً):
const slow = new Promise(resolve => setTimeout(() => resolve('slow'), 3000));
const fast = new Promise(resolve => setTimeout(() => resolve('fast'), 1000));
Promise.race([slow, fast])
.then(result => {
console.log('Winner:', result); // 'fast'
});
// استخدام عملي: نمط المهلة
async function fetchWithTimeout(url, timeout = 5000) {
const fetchPromise = fetch(url);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Request timeout')), timeout);
});
try {
const response = await Promise.race([fetchPromise, timeoutPromise]);
return response;
} catch (error) {
console.error('Fetch failed:', error.message);
throw error;
}
}
Promise.any() - أول نجاح
يعيد أول وعد يُحل بنجاح:
const promises = [
Promise.reject('Error 1'),
new Promise(resolve => setTimeout(() => resolve('Success 2'), 1000)),
new Promise(resolve => setTimeout(() => resolve('Success 3'), 500))
];
Promise.any(promises)
.then(result => {
console.log('First success:', result); // 'Success 3'
})
.catch(error => {
// يُستدعى فقط إذا رُفضت جميع الوعود
console.error('All failed:', error);
});
تمرين تطبيقي
- أنشئ دالة تعيد Promise يُحل بعد تأخير
- حوّل هذه الدالة لاستخدام async/await
- أنشئ ثلاث دوال async تحاكي استدعاءات API بتأخيرات مختلفة
- استخدم
Promise.all() لتشغيلها بالتوازي وقياس الوقت
- نفّذ معالجة الأخطاء باستخدام try/catch
- استخدم
Promise.race() لتنفيذ مهلة لطلب fetch
- أنشئ دالة تعيد محاولة عملية async فاشلة 3 مرات قبل الاستسلام
ملخص
في هذا الدرس، تعلمت:
- كيف تمكّن حلقة الأحداث العمليات غير المتزامنة في Node.js
- ردود النداء ومشكلة جحيم رد النداء
- الوعود: الإنشاء، الربط، ومعالجة الأخطاء
- صيغة Async/await لكود غير متزامن أنظف
- معالجة الأخطاء باستخدام كتل try/catch
- تشغيل العمليات المتوازية باستخدام
Promise.all()
- مُجمِّعات Promise المختلفة:
allSettled()، race()، any()
- أنماط التنفيذ المتسلسل مقابل المتوازي