دليل تعلم جافاسكربت اللاتزامن والانتظار async/await في جافاسكربت


صفا الفليج

توجد صياغة مميّزة للعمل مع الوعود بنحوٍ أكثر سهولة تُدعى 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:

  1. علينا استبدال استدعاءات ‎.then ووضع await.
  2. علينا أيضًا تحويل الدالة لتكون غير متزامنة 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 المفتاحية قبل الدوال تأثيرين اثنين:

  1. تحوّلها لتُعيد وعدًا دومًا.
  2. تتيح استعمال await فيها.

حين يرى محرّك جافاسكربت الكلمة المفتاحية await قبل الوعود، ينتظر حتّى يُنجز الوعد ومن ثمّ:

  1. لو كان خطأ فسيُولِّد الاستثناء كما لو استعملنا throw error.
  2. وإلّا أعاد الناتج.

تقدّم لنا هتين الكلمتين معًا إطار عمل رائع نكتب به شيفرات غير متزامنة تسهل علينا قراءتها كما وكتابتها.

نادرًا ما نستعمل 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)

ملاحظات:

  1. الدالّة loadJson تصبح async.

  2. جميع المحتوى في .then يستبدل بـِ await.

  3. يمكننا إعادة return response.json()‎ بدلًا من انتظارها. هكذا:

    if (response.status == 200) {
      return response.json(); // (3)
    }

    ثم الشيفرة الخارجية ستنتظر await لينفذ الوعد في حالتنا الأمر غير مهم.

  4. سيرمى الخطأ من التابع 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





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن