الدوال غير المتزامنة: تقديم وعود ودية

تسمح لك الدوال غير المتزامنة بكتابة رمز يستنِد إلى وعود كما لو كان متزامنًا.

يتم تفعيل الدوال غير المتزامنة تلقائيًا في Chrome وEdge وFirefox وSafari، وهي رائعة بصراحة. وتسمح لك بكتابة رمز برمجي يستند إلى الوعود كما هو الحال مع الرموز البرمجية المتزامنة، ولكن بدون حظر سلسلة التعليمات الرئيسية. وتجعل هذه الوظائف تعليماتك البرمجية غير المتزامنة أقل "ذكاءً" وأكثر سهولة في القراءة.

تعمل الدوال غير المتزامنة على النحو التالي:

async function myFirstAsyncFunction() {
  try {
    const fulfilledValue = await promise;
  } catch (rejectedValue) {
    // …
  }
}

إذا كنت تستخدم الكلمة الرئيسية async قبل تعريف الدالة، يمكنك استخدام await داخل الدالة. عند await وعود، يتم إيقاف الوظيفة مؤقتًا بطريقة غير مقيدة إلى أن يستقر الوعد. إذا وفاء بالوعد، فستعود القيمة. إذا رفض الوعد، يتم طرح القيمة المرفوضة.

دعم المتصفح

توافق المتصفّح

  • Chrome: 55
  • Edge: 15.
  • Firefox: 52
  • ‫Safari: 10.1

المصدر

مثال: تسجيل عملية استرجاع

لنفترض أنك تريد جلب عنوان URL وتسجيل الرد كنص. إليك كيف يبدو باستخدام الوعود:

function logFetch(url) {
  return fetch(url)
    .then((response) => response.text())
    .then((text) => {
      console.log(text);
    })
    .catch((err) => {
      console.error('fetch failed', err);
    });
}

إليك الإجراء نفسه باستخدام الدوال غير المتزامنة:

async function logFetch(url) {
  try {
    const response = await fetch(url);
    console.log(await response.text());
  } catch (err) {
    console.log('fetch failed', err);
  }
}

إنه العدد نفسه من الأسطر، ولكن تمّ حذف جميع عمليات تسجيل المكالمات. ما يجعلها أسهل بكثير في القراءة، خاصةً بالنسبة إلى المستخدمين الذين لا يعرفون الكثير عن الوعود.

قيم الإرجاع غير المتزامنة

تعرض الدوال غير المتزامنة دائمًا وعدًا، سواء كنت تستخدم await أم لا. ويحل هذا الوعد بكل ما تعرضه الدالة غير المتزامنة أو ترفضه مهما كان ما تطرحه الدالة غير المتزامنة. وبالتالي، مع:

// wait ms milliseconds
function wait(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function hello() {
  await wait(500);
  return 'world';
}

...الاتصال بـ hello() يؤدي إلى إرجاع وعد يفي بـ "world".

async function foo() {
  await wait(500);
  throw Error('bar');
}

…يؤدي استدعاء foo() إلى عرض وعد يرفض باستخدام Error('bar').

مثال: بث استجابة

تزداد فائدة الدوال غير المتزامنة في الأمثلة الأكثر تعقيدًا. لنفترض أنك أردت بث رد أثناء تسجيل الخروج من المقاطع، وإرجاع الحجم النهائي.

في ما يلي بعض الوعود:

function getResponseSize(url) {
  return fetch(url).then((response) => {
    const reader = response.body.getReader();
    let total = 0;

    return reader.read().then(function processResult(result) {
      if (result.done) return total;

      const value = result.value;
      total += value.length;
      console.log('Received chunk', value);

      return reader.read().then(processResult);
    });
  });
}

ارجع إلى "جيك" وهو "حامل الوعود". هل يمكنك ملاحظة كيف أستدعي processResult() داخل نفسه لإعداد حلقة غير متزامنة؟ كتابة جعلتني أشعر بذكاء كبير ولكن مثل معظم الرموز "الذكية"، عليك التحديق فيها لعدة دقائق لمعرفة ما تفعله، مثل إحدى صور العين السحرية من التسعينيات.

لنحاول ذلك مرة أخرى باستخدام الدوالّ غير المتزامنة:

async function getResponseSize(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  let result = await reader.read();
  let total = 0;

  while (!result.done) {
    const value = result.value;
    total += value.length;
    console.log('Received chunk', value);
    // get the next result
    result = await reader.read();
  }

  return total;
}

اختفت كل العناصر "الذكية". تم استبدال الحلقة غير المتزامنة التي جعلتني أشعر بالانعدام بحل بديل موثوق به وممل. أفضل بكثير. في المستقبل، ستحصل على مكررات غير متزامنة، التي بدورها ستستبدل حلقة while بحلقة for-of، ما يجعلها أكثر اتساقًا.

بنية الدالة غير المتزامنة الأخرى

لقد عرّفناك على async function() {} من قبل، ولكن يمكن استخدام الكلمة الرئيسية async مع بنية دالة أخرى:

الدوال السهمية

// map some URLs to json-promises
const jsonPromises = urls.map(async (url) => {
  const response = await fetch(url);
  return response.json();
});

طرق العناصر

const storage = {
  async getAvatar(name) {
    const cache = await caches.open('avatars');
    return cache.match(`/avatars/${name}.jpg`);
  }
};

storage.getAvatar('jaffathecake').then(…);

طرق الصف

class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jaffathecake').then(…);

تنبيه: تجنب التتابع كثيرًا

على الرغم من أنّك تكتب رمزًا برمجيًا يبدو متزامنًا، تأكَّد من عدم تفويت فرصة تنفيذ المهام بشكل موازٍ.

async function series() {
  await wait(500); // Wait 500ms…
  await wait(500); // …then wait another 500ms.
  return 'done!';
}

يستغرق إكمال ما سبق 1000 ملي ثانية، في حين أنّ:

async function parallel() {
  const wait1 = wait(500); // Start a 500ms timer asynchronously…
  const wait2 = wait(500); // …meaning this timer happens in parallel.
  await Promise.all([wait1, wait2]); // Wait for both timers in parallel.
  return 'done!';
}

يستغرق إكمال ما سبق 500 ملي ثانية، لأنّ كلا المرحلتَين تحدثان في الوقت نفسه. لنلقِ نظرة على مثال عملي.

مثال: عرض عمليات الجلب بالترتيب

لنفترض أنّك أردت جلب سلسلة من عناوين URL وتسجيلها في أقرب وقت ممكن بالترتيب الصحيح.

تنفس عميق - إليك كيف يبدو ذلك مع الوعود:

function markHandled(promise) {
  promise.catch(() => {});
  return promise;
}

function logInOrder(urls) {
  // fetch all the URLs
  const textPromises = urls.map((url) => {
    return markHandled(fetch(url).then((response) => response.text()));
  });

  // log them in order
  return textPromises.reduce((chain, textPromise) => {
    return chain.then(() => textPromise).then((text) => console.log(text));
  }, Promise.resolve());
}

نعم، هذا صحيح، أستخدم reduce لربط سلسلة من الوعود. أنا ذكي جدًا . ولكن هذا رمز ذكي جدًا، ومن الأفضل عدم استخدامه.

ومع ذلك، عند تحويل الرمز أعلاه إلى دالة غير متزامنة، من المغري أن تصبح متسلسلة جدًا:

خيار غير مستحسن - تسلسلي جدًا
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
يبدو هذا الإجراء أكثر دقة، ولكن لا يبدأ جلب البيانات الثاني إلا بعد قراءة عملية الجلب الأولى بالكامل، وهكذا. وهذا أبطأ بكثير من مثال الوعد الذي يؤدي عمليات الجلب بالتوازي. لحسن الحظ، هناك حلّ وسط مثالي.
موصى به - رائع وموازي
function markHandled(...promises) {
  Promise.allSettled(promises);
}

async function logInOrder(urls) {
  // fetch all the URLs in parallel
  const textPromises = urls.map(async (url) => {
    const response = await fetch(url);
    return response.text();
  });

  markHandled(...textPromises);

  // log them in sequence
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}
في هذا المثال، يتم جلب عناوين URL وقراءتها بشكل موازٍ، ولكن يتم استبدال القسم "الذكي" reduce بحلقة for عادية ومملة وقابلة للقراءة.

حلّ بديل لتوفير إمكانية استخدام المتصفّحات: أدوات إنشاء

إذا كنت تستهدِف المتصفّحات التي تتيح استخدام المولدات (بما في ذلك أحدث إصدار من كل متصفّح رئيسي )، يمكنك استخدام polyfill لوظائف غير المتزامنة.

سينفّذ Babel هذا الإجراء نيابةً عنك، في ما يلي مثال على ذلك من خلال Babel REPL

أنصح باستخدام نهج التحويل البرمجي، لأنّه يمكنك إيقافه بعد أن يصبح المتصفّحات المستهدَفة متوافقة مع الوظائف غير المتزامنة، ولكن إذا كنت حقًا لا تريد استخدام أداة تحويل برمجي، يمكنك استخدام البوليفيلر من Babel بنفسك. بدلاً من:

async function slowEcho(val) {
  await wait(1000);
  return val;
}

…يمكنك تضمين البوليفيل وكتابة ما يلي:

const slowEcho = createAsyncFunction(function* (val) {
  yield wait(1000);
  return val;
});

يُرجى العِلم أنّه عليك تمرير مُنشئ (function*) إلى createAsyncFunction، واستخدام yield بدلاً من await. بخلاف ذلك، تعمل الطريقة نفسها.

الحلّ: regenerator

إذا كنت تستهدِف المتصفحات القديمة، يمكن أن تُحوِّل Babel أيضًا أدوات إنشاء السلسلة، مما يتيح لك استخدام الدوالّ غير المتزامنة حتى IE8. لإجراء ذلك، تحتاج إلى الإعداد المسبق es2017 في Babel وes2015 مسبقًا.

الناتج ليس بهذه السهولة، لذا احترس من تكديس الرموز.

Async كل الأشياء

بعد توفّر الدوال غير المتزامنة على جميع المتصفحات، استخدِمها في كل دالة تُعرِض وعدًا. ولا يؤدي ذلك إلى ترتيب الرمز البرمجي فحسب، بل يؤدي أيضًا إلى ضمان أن تعرِض الدالة دائمًا وعدًا.

لقد كنت متحمسًا بشأن الدوال غير المتزامنة في عام 2014، ويسعدني أن أراها يتم تطبيقها، في الواقع، في المتصفحات. رائع.