توجد صياغة مميّزة للعمل مع الوعود بنحوٍ أكثر سهولة تُدعى async/await
. فهمها أسهل من شرب الماء واستعمالها
الدوال غير المتزامنة
فلنبدأ أولًا بكلمة async
المفتاحية. يمكننا وضعها قبل الدوال هكذا:
async function f() { return 1; }
وجود الكلمة ”async“ قبل (اختصار ”غير متزامنة“ بالإنجليزية) يعني أمرًا واحدًا: تُعيد الدالة وعدًا دومًا.
فمثلًا تُعيد هذه الدالة وعدًا مُنجز فيه ناتج 1
. فلنرى:
async function f() { return 1; } f().then(alert); // 1
كما يمكننا أيضًا إعادة وعد صراحةً:
async function f() { return Promise.resolve(1); } f().then(alert); // 1
هكذا تضمن لنا async
بأنّ الدالة ستُعيد وعدًا وستُغلّف الأشياء التي ليست بوعود وعودًا. بسيطة صح؟ ليس هذا فحسب، بل هناك أيضًا الكلمة المفتاحية await
التي تعمل فقط في الدوال غير المتزامنة async
، وهي الأخرى جميلة.
Await
الصياغة:
// لا تعمل إلّا في الدوال غير المتزامنة let value = await promise;
الكلمة المفتاحية تجعل لغة جافاسكربت تنتظر حتى يُنجز الوعد ويعيد نتيجة.
إليك مثالًا لوعد نُفذّ خلال ثانية واحدة:
async function f() { let promise = new Promise((resolve, reject) => { setTimeout(() => resolve("done!"), 1000) // تم! }); let result = await promise; // ننتظر ... الوعد (*) alert(result); // "done!" تم! } f();
يتوقّف تنفيذ الدالة ”قليلًا“ عند السطر (*)
ويتواصل متى ما أُنجز الوعد وصار result
ناتجه. الشيفرة أعلاه تعرض ”تم!“ بعد ثانية واحدة.
لنوضح أمرًا مهمًا: تجعل await
لغة جافاسكربت تنتظر حتى يُنجز الوعد، وبعدها تذهب مع النتيجة. هذا لن يكلفنا أي موارد من المعالج لأن المحرك مُنشغل بمهام أخرى في الوقت نفسه، مثل: تنفيذ سكربتات أُخرى، التعامل مع الأحداث ..إلخ.
هي مجرد صياغة أنيقة أكثر من صياغة promise.then
للحصول على ناتج الوعد. كما أنها أسهل للقراءة والكتابة..
تحذير: لا يمكننا استخدام await
في الدوال العادية إذا حاولنا استخدام الكلمة المفتاحية await
في الدوال العادية فسيظهر خطأ في في الصياغة:
function f() { let promise = Promise.resolve(1); let result = await promise; // Syntax error }
سنحصل على هذا الخطأ إذا لم نضع async
قبل الدالّة، كما قلنا await
تعمل فقط في الدوالّ غير المتزامنة.
لنأخذ مثال showAvatar()
من الفصل سَلسلة الوعود ونُعيد كتابته باستعمال async/await
:
-
علينا استبدال استدعاءات
.then
ووضعawait
. -
علينا أيضًا تحويل الدالة لتكون غير متزامنة
async
كي تعملawait
.
async function showAvatar() { // إقرأ ملفات JSON let response = await fetch('/article/promise-chaining/user.json'); let user = await response.json(); // إقرأ مستخدم github let githubResponse = await fetch(`https://api.github.com/users/${user.name}`); let githubUser = await githubResponse.json(); // أظهرالصورة الرمزية avatar let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); // انتظر 3 ثواني await new Promise((resolve, reject) => setTimeout(resolve, 3000)); img.remove(); return githubUser; } showAvatar();
شيفرة نظيفة وسهلة القراءة! أفضل من السابقة بأميال.
ملاحظة: لن تعمل await
في الشيفرة ذات المستوى الأعلى يميل المبتدئين إلى نسيان أن await
لن تعمل في الشيفرة البرمجية ذات هرمية أعلى(ذات المستوى الأعلى). فمثلًا لن يعمل هذا:
// خطأ صياغيّ في الشيفرة عالية المستوى let response = await fetch('/article/promise-chaining/user.json'); let user = await response.json();
يمكننا تغليفها بداخل دالّة متزامنة مجهولة، هكذا:
(async () => { let response = await fetch('/article/promise-chaining/user.json'); let user = await response.json(); ... })();
ملاحظة: إن await
تقبل "thenables" تسمح await
باستخدام كائنات thenable (تلك التي تستخدم دالة then
القابلة للاستدعاء) تمامًا مثل promise.then
. الفكرة أنه يمكن ألا يكون الكائن الخارجي وعدًا ولكنه متوافق مع الوعد: إن كان يدعم .then
، وهذا يكفي لاستخدامه مع await
.
هنا مثال لاستخدام صنف Thenable
والكلمة المفتاحية await
أدناه ستقبلُ حالاتها:
class Thenable { constructor(num) { this.num = num; } then(resolve, reject) { alert(resolve); // نفذ عملية this.num*2 بعد ثانية واحدة setTimeout(() => resolve(this.num * 2), 1000); // (*) } }; async function f() { // انتظر لثانية واحدة. ثم النتيجة ستصبح 2 let result = await new Thenable(1); alert(result); } f();
إذا حصَلت await
على كائن ليس وعدًا مع .then
فإنه يستدعي الدوالّ المضمنة في اللغة مثل: resolve
و reject
كوسطاء (كما يفعل المنفذّ للوعد العادي تمامًا). وثم تنتظر await
حتى يستدعى أحدهم (في المثال أعلاه في السطر (*)
) ثم يواصل مع النتيجة.
ملاحظة: لتعريف دالّة صنف غير متزامنة إضفها مع الكلمة async
:
class Waiter { async wait() { return await Promise.resolve(1); } } new Waiter() .wait() .then(alert); // 1
لاحظ أن المعنى هو نفسه: فهو يضمن أن القيمة المرتجعة هي وعد وawait
مفعّلة أيضًا.
التعامل مع الأخطاء
لو نُفذّ الوعد بنجاح فسيُعيد await promise
الناتج، ولكن لو حصلت حالة رفض فسترمي الخطأ كما لو كانت إفادة throw
مكتوبة.
إليك الشيفرة:
async function f() { await Promise.reject(new Error("Whoops!")); // لاحظ }
و… هي ذاتها هذه:
async function f() { throw new Error("Whoops!"); // هنا }
في الواقع تأخذ الوعود وقتًا قبل أن تُرفض. في تلك لحالة فستكون هناك مهلة قبل أن يرمي await
الخطأ.
يمكننا التقاط ذاك الخطأ باستعمال try..catch
كما استعملنا throw
:
async function f() { try { let response = await fetch('http://no-such-url'); } catch(err) { alert(err); // TypeError: failed to fetch خطأ في النوع: فشل الجلب } } f();
لو حدث خطأ فينتقل سير التنفيذ إلى كتلة catch
. يمكننا أيضًا وضع أكثر من سطر واحد:
async function f() { try { let response = await fetch('/no-user-here'); let user = await response.json(); } catch(err) { // تلتقط أخطاء fetch وresponse.json معًا alert(err); } } f();
لو لم نضع try..catch
فستكون حالة الوعد الذي نفّذه استدعاء الدالة غير المتزامنة f()
- يكون بحالة رفض. يمكننا هنا استعمال .catch
للتعامل معه:
async function f() { let response = await fetch('http://no-such-url'); } // يصير استدعاء f() وعدًا مرفوضًا f().catch(alert); // TypeError: failed to fetch خطأ في النوع: فشل الجلب (*)
لو نسينا هنا إضافة .catch
فسنتلقّى خطأ وعود لم نتعامل معه (يمكن أن نراه من الطرفية). يمكننا أيضًا استلام هذه الأخطاء باستعمال دالة مُعاملة الأحداث العمومية كما وضّحنا في الفصل ….
ملاحظة: "async/await
وpromise.then/catch
" عندما نستخدم async/await
نادرًا ما نحتاج إلى .then
وذلك لأن await
تعالج عملية الانتظار. ويمكننا استخدام try..catch
بدلًا من .catch
. وهذه عادةً (ليس دائمًا) ما تكون أكثر ملاءمة.
ولكن في الشيفرات ذات هرمية أعلى (مستوى أعلى)، عندما نكون خارج أي دالّة غير متزامنة، يتعذر علينا استخدام await
لذا من المعتاد إضافة .then/catch
لمعالجة النتيجة النهائية أو الأخطاء المتساقطة.
كما هو الحال في السطر (*)
من المثال أعلاه.
ملاحظة: "تعمل async/await
مع Promise.all
" عندما نحتاج لانتظار عدة وعود يمكننا أن نغلفها في تابع Promise.all
وبعده await
:
// انتظر مصفوفة النتائج let results = await Promise.all([ fetch(url1), fetch(url2), ... ]);
وإن حدث خطأ ما، فإنه سيُنقل من الوعد نفسه إلى التابع Promise.all
، وبعدها يصبح استثناءً يمكننا التقاطه باستخدام try..catch
حول الاستدعاء.
خلاصة
لكلمة async
المفتاحية قبل الدوال تأثيرين اثنين:
- تحوّلها لتُعيد وعدًا دومًا.
-
تتيح استعمال
await
فيها.
حين يرى محرّك جافاسكربت الكلمة المفتاحية await
قبل الوعود، ينتظر حتّى يُنجز الوعد ومن ثمّ:
-
لو كان خطأ فسيُولِّد الاستثناء كما لو استعملنا
throw error
. - وإلّا أعاد الناتج.
تقدّم لنا هتين الكلمتين معًا إطار عمل رائع نكتب به شيفرات غير متزامنة تسهل علينا قراءتها كما وكتابتها.
نادرًا ما نستعمل promise.then/catch
بوجود async/await
، ولكن علينا ألّا ننسى بأنّ الأخيرتين مبنيّتين على الوعود إذ نضطر أحيانًا (خارج الدوال مثلًا) استعمال promise.then/catch
. كما وأنّ Promise.all
جميل جدًا لننتظر أكثر من مهمّة في وقت واحد.
تمارين
إعادة الكتابة باستعمال async/await
أعِد كتابة الشيفرة في المثال من الفصل سلسلة الوعود باستعمال async/await
بدل .then/catch
:
function loadJson(url) { return fetch(url) .then(response => { if (response.status == 200) { return response.json(); } else { throw new Error(response.status); } }) } loadJson('no-such-user.json') // (3) .catch(alert); // Error: 404
الحل
ترى الملاحظات أسفل الشيفرة:
async function loadJson(url) { // (1) let response = await fetch(url); // (2) if (response.status == 200) { let json = await response.json(); // (3) return json; } throw new Error(response.status); } loadJson('no-such-user.json') .catch(alert); // Error: 404 (4)
ملاحظات:
-
الدالّة
loadJson
تصبحasync
. -
جميع المحتوى في
.then
يستبدل بـِawait
. -
يمكننا إعادة
return response.json()
بدلًا من انتظارها. هكذا:if (response.status == 200) { return response.json(); // (3) }
ثم الشيفرة الخارجية ستنتظر
await
لينفذ الوعد في حالتنا الأمر غير مهم. -
سيرمى الخطأ من التابع
loadJson
المعالج من قبل.catch
. لن نستطيع استخدامawait loadJson(…)
هنا، وذلك لأننا لسنا في دالّة غير متزامنة.
أعِد كتابة rethrow
باستعمال async/await
في الشيفرة أدناه مثلًا عن إعادة الرمي من فصل سلسلة الوعود. أعد كتابته باستخدام async/await
بدلًا من .then/catch
. وتخلص من العودية لصالح الحلقة في demoGithubUser
: مع استخدام async/await
سيسهلُ الأمر.
class HttpError extends Error { constructor(response) { super(`${response.status} for ${response.url}`); this.name = 'HttpError'; this.response = response; } } function loadJson(url) { return fetch(url) .then(response => { if (response.status == 200) { return response.json(); } else { throw new HttpError(response); } }) } // اطلب اسم المستخدم إلى أن يعيد لك github مستخدم صحيح function demoGithubUser() { let name = prompt("Enter a name?", "iliakan"); return loadJson(`https://api.github.com/users/${name}`) .then(user => { alert(`Full name: ${user.name}.`); return user; }) .catch(err => { if (err instanceof HttpError && err.response.status == 404) { alert("No such user, please reenter."); return demoGithubUser(); } else { throw err; } }); } demoGithubUser();
الحل
لا يوجد أي صعوبة هنا، إذ كل ما عليك فعله هو استبدال try...catch
بدلًا من .catch
بداخل تابع demoGithubUser
وأضف async/await
عند الحاجة:
class HttpError extends Error { constructor(response) { super(`${response.status} for ${response.url}`); this.name = 'HttpError'; this.response = response; } } async function loadJson(url) { let response = await fetch(url); if (response.status == 200) { return response.json(); } else { throw new HttpError(response); } } // اطلب اسم المستخدم إلى أن يعيد لك github مستخدم صحيح async function demoGithubUser() { let user; while(true) { let name = prompt("Enter a name?", "iliakan"); try { user = await loadJson(`https://api.github.com/users/${name}`); break; // لا يوجد خطأ اخرج من الحلقة } catch(err) { if (err instanceof HttpError && err.response.status == 404) { // تستمر الحلقة بعد alert alert("No such user, please reenter."); } else { // خطأ غير معروف , أعد رميه rethrow throw err; } } } alert(`Full name: ${user.name}.`); return user; } demoGithubUser();
استدعاء async من دالة غير متزامنة
لدينا دالة ”عادية“، ونريد استدعاء async
منها واستعمال ناتجها، كيف؟
async function wait() { await new Promise(resolve => setTimeout(resolve, 1000)); return 10; } function f() { // ...ماذا نكتب هنا؟ // علينا استدعاء async wait() والانتظار حتّى تأتي 10 // لا تنسَ، لا يمكن استعمال ”await“ هنا }
ملاحظة: هذه المهمّة (تقنيًا) بسيطة جدًا، ولكنّ السؤال شائع بين عموم المطوّرين الجدد على async/await.
الحل
هنا تأتي فائدة معرفة طريقة عمل هذه الأمور.
ليس عليك إلّا معاملة استدعاء async
وكأنّه وعد ووضع تابِع .then
:
async function wait() { await new Promise(resolve => setTimeout(resolve, 1000)); return 10; } function f() { // يعرض 10 بعد ثانية واحدة wait().then(result => alert(result)); } f();
ترجمة -وبتصرف- للفصل Async/await من كتاب The JavaScript language
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.