Async/Await - البرمجة غير المتزامنة المبسطة
Async/await هو بناء جملة JavaScript حديث يجعل الكود غير المتزامن يبدو ويتصرف مثل الكود المتزامن. إنه مبني على أساس Promises لكنه يوفر طريقة أنظف وأكثر قابلية للقراءة للعمل مع العمليات غير المتزامنة.
فهم الدوال غير المتزامنة (Async Functions)
تُستخدم الكلمة المفتاحية async للإعلان عن دالة غير متزامنة. تُرجع الدالة غير المتزامنة دائماً Promise:
// دالة عادية
function regularFunction() {
return "Hello";
}
// دالة غير متزامنة - تلف القيمة المرجعة تلقائياً في Promise
async function asyncFunction() {
return "Hello";
}
console.log(regularFunction()); // "Hello"
console.log(asyncFunction()); // Promise {<fulfilled>: "Hello"}
// للحصول على القيمة، استخدم .then() أو await
asyncFunction().then(result => console.log(result)); // "Hello"
نقطة مهمة: أي دالة معلنة بـ async تُرجع تلقائياً Promise. إذا أرجعت قيمة، فإنها تُلف في Promise محلول. إذا رميت خطأ، فإنه يُلف في Promise مرفوض.
الكلمة المفتاحية Await
يمكن استخدام الكلمة المفتاحية await فقط داخل الدوال غير المتزامنة. تُوقف تنفيذ الدالة غير المتزامنة وتنتظر حل Promise:
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function demo() {
console.log("Starting...");
await delay(2000); // توقف لمدة ثانيتين
console.log("After 2 seconds");
await delay(1000); // توقف لمدة ثانية إضافية
console.log("After 3 seconds total");
}
demo();
// المخرجات:
// Starting... (فوراً)
// After 2 seconds (بعد ثانيتين)
// After 3 seconds total (بعد 3 ثواني)
معالجة الأخطاء باستخدام Try/Catch
مع async/await، يمكننا استخدام كتل try/catch التقليدية لمعالجة الأخطاء، وهو أكثر سهولة من Promise .catch():
async function fetchUserData(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Error fetching user:", error.message);
throw error; // إعادة رمي الخطأ إذا لزم الأمر
}
}
// استخدام الدالة
async function displayUser() {
try {
const user = await fetchUserData(123);
console.log("User:", user);
} catch (error) {
console.log("Failed to load user");
}
}
أفضل ممارسة: دائماً لف استدعاءات await في كتل try/catch للتعامل مع الأخطاء المحتملة بشكل سلس. هذا يمنع رفض Promise غير المعالج.
Async/Await مقابل Promises
إليك نفس الكود مكتوباً بـ Promises ثم بـ async/await:
// استخدام Promises (أصعب في القراءة)
function getUserPosts(userId) {
return fetch(`https://api.example.com/users/${userId}`)
.then(response => response.json())
.then(user => {
return fetch(`https://api.example.com/posts?userId=${user.id}`);
})
.then(response => response.json())
.then(posts => {
return posts;
})
.catch(error => {
console.error("Error:", error);
throw error;
});
}
// استخدام Async/Await (أنظف بكثير!)
async function getUserPosts(userId) {
try {
const userResponse = await fetch(`https://api.example.com/users/${userId}`);
const user = await userResponse.json();
const postsResponse = await fetch(`https://api.example.com/posts?userId=${user.id}`);
const posts = await postsResponse.json();
return posts;
} catch (error) {
console.error("Error:", error);
throw error;
}
}
التنفيذ المتسلسل مقابل التنفيذ المتوازي
فهم متى تعمل العمليات بشكل متسلسل أو متوازي أمر حاسم للأداء:
// متسلسل (أبطأ - ينتظر كل طلب)
async function getDataSequential() {
const user = await fetch("/api/user");
const posts = await fetch("/api/posts");
const comments = await fetch("/api/comments");
return {
user: await user.json(),
posts: await posts.json(),
comments: await comments.json()
};
}
// الوقت الكلي: time1 + time2 + time3
// متوازي (أسرع - جميع الطلبات تبدأ فوراً)
async function getDataParallel() {
// ابدأ جميع الطلبات مرة واحدة
const [userRes, postsRes, commentsRes] = await Promise.all([
fetch("/api/user"),
fetch("/api/posts"),
fetch("/api/comments")
]);
return {
user: await userRes.json(),
posts: await postsRes.json(),
comments: await commentsRes.json()
};
}
// الوقت الكلي: max(time1, time2, time3)
نصيحة للأداء: استخدم await المتسلسل فقط عندما تعتمد عملية على نتيجة عملية أخرى. خلاف ذلك، استخدم Promise.all() لتشغيل العمليات بالتوازي لأداء أفضل.
الأنماط الشائعة وأفضل الممارسات
// ✓ جيد: عمليات مستقلة متوازية
async function loadDashboard() {
const [user, stats, notifications] = await Promise.all([
fetchUser(),
fetchStats(),
fetchNotifications()
]);
return { user, stats, notifications };
}
// ✗ سيء: عمليات مستقلة متسلسلة
async function loadDashboardSlow() {
const user = await fetchUser();
const stats = await fetchStats(); // ينتظر دون داعي
const notifications = await fetchNotifications(); // ينتظر دون داعي
return { user, stats, notifications };
}
// ✓ جيد: عمليات معتمدة متسلسلة
async function createUserAndProfile(userData) {
const user = await createUser(userData);
const profile = await createProfile(user.id); // يحتاج user.id
return { user, profile };
}
// ✓ جيد: معالجة الأخطاء برسائل محددة
async function processPayment(orderId) {
try {
const order = await fetchOrder(orderId);
const payment = await chargeCard(order.total);
await sendConfirmation(order.email);
return payment;
} catch (error) {
if (error.message.includes("card")) {
throw new Error("فشل الدفع. يرجى التحقق من تفاصيل بطاقتك.");
}
throw new Error("فشلت معالجة الطلب. يرجى المحاولة مرة أخرى.");
}
}
Async/Await مع دوال المصفوفات
كن حذراً عند استخدام async/await مع دوال المصفوفات مثل map() و forEach():
// ✗ خطأ: forEach لا تنتظر العمليات غير المتزامنة
async function processUsersWrong(userIds) {
userIds.forEach(async (id) => {
const user = await fetchUser(id);
console.log(user);
}); // تكتمل فوراً دون انتظار
}
// ✓ صحيح: استخدم حلقة for...of
async function processUsersCorrect(userIds) {
for (const id of userIds) {
const user = await fetchUser(id);
console.log(user);
}
}
// ✓ صحيح: استخدم Promise.all مع map للمعالجة المتوازية
async function processUsersParallel(userIds) {
const users = await Promise.all(
userIds.map(id => fetchUser(id))
);
users.forEach(user => console.log(user));
}
مثال من العالم الحقيقي: تدفق تسجيل المستخدم
async function registerUser(formData) {
try {
// 1. التحقق من أن البريد الإلكتروني غير مستخدم
const emailExists = await checkEmailExists(formData.email);
if (emailExists) {
throw new Error("البريد الإلكتروني مسجل بالفعل");
}
// 2. إنشاء حساب المستخدم
const user = await createUserAccount(formData);
// 3. يمكن تشغيل هذه بالتوازي
await Promise.all([
sendWelcomeEmail(user.email),
createUserProfile(user.id),
logRegistrationEvent(user.id)
]);
// 4. توليد رمز الجلسة
const token = await generateAuthToken(user.id);
return { user, token };
} catch (error) {
console.error("فشل التسجيل:", error.message);
throw error;
}
}
// الاستخدام
async function handleRegistration(formData) {
try {
const result = await registerUser(formData);
console.log("نجح التسجيل!");
return result;
} catch (error) {
alert(`فشل التسجيل: ${error.message}`);
}
}
تمرين تطبيقي:
المهمة: أنشئ دالة غير متزامنة تجلب مستخدماً ومنشوراته، ثم تضيف عدد التعليقات لكل منشور.
// مهمتك: تنفيذ هذه الدالة
async function getUserWithPostStats(userId) {
// 1. اجلب بيانات المستخدم
// 2. اجلب منشورات المستخدم
// 3. لكل منشور، اجلب عدد التعليقات
// 4. أرجع المستخدم مع المنشورات متضمنة عدد التعليقات
}
// بنية المخرجات المتوقعة:
// {
// user: { id: 1, name: "John" },
// posts: [
// { id: 1, title: "Post 1", commentCount: 5 },
// { id: 2, title: "Post 2", commentCount: 3 }
// ]
// }
الحل:
async function getUserWithPostStats(userId) {
try {
// اجلب المستخدم والمنشورات بالتوازي
const [userRes, postsRes] = await Promise.all([
fetch(`https://api.example.com/users/${userId}`),
fetch(`https://api.example.com/posts?userId=${userId}`)
]);
const user = await userRes.json();
const posts = await postsRes.json();
// اجلب عدد التعليقات لجميع المنشورات بالتوازي
const postsWithComments = await Promise.all(
posts.map(async (post) => {
const commentsRes = await fetch(
`https://api.example.com/comments?postId=${post.id}`
);
const comments = await commentsRes.json();
return {
...post,
commentCount: comments.length
};
})
);
return { user, posts: postsWithComments };
} catch (error) {
console.error("خطأ:", error);
throw error;
}
}
الملخص
في هذا الدرس، تعلمت:
- الدوال غير المتزامنة تُرجع Promises تلقائياً
- Await يوقف التنفيذ حتى يتم حل Promise
- كتل Try/catch تعالج الأخطاء في الدوال غير المتزامنة
- Async/await يوفر بناء جملة أنظف من سلاسل Promise
- استخدم Promise.all() للعمليات المتوازية
- await المتسلسل يجب استخدامه فقط للعمليات المعتمدة
- كن حذراً مع العمليات غير المتزامنة في دوال المصفوفات
التالي: في الدرس التالي، سنستكشف Fetch API ونتعلم كيفية إجراء طلبات HTTP باستخدام async/await!