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

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

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

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

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

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

دعم المتصفح

دعم المتصفح

  • Chrome: 55.
  • الحافة: 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 Loop، ما يجعلها أكثر إتقانًا.

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

لقد عرضتُ لك 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!';
}

يستغرق اكتمال العملية أعلاه 1, 000 ملّي ثانية، في حين أنّ:

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 بت بحلقة عادية ومملة وقابلة للقراءة.

حل بديل لدعم المتصفّح: أدوات إنشاء التصاميم

إذا كنت تستهدف المتصفحات المتوافقة مع المنشئات (التي تتضمن باستخدام أحدث إصدار من كل متصفح رئيسي ) يمكنك تصنيف دوال polyfill غير المتزامنة.

ستتولى Babel هذه المهمة نيابةً عنك. إليك مثال من خلال مبادرة Babel REPL.

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

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

...ستضمِّن الرموز polyfill والكتابة:

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

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

الحل: إعادة الإنشاء

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

الناتج ليس جميلاً، لذا احترس من تكبير/تصغير التعليمات البرمجية.

عدم مزامنة جميع العناصر

بعد أن تظهر الدوال غير المتزامنة على جميع المتصفحات، استخدِمها في كل ذات تأثير إيجابي! فهي لا تجعل التعليمة البرمجية أكثر تنظيمًا فحسب، بل إنها تجعل التأكد من أن هذه الدالة ستقدم وعودًا دائمًا.

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