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

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

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

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

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

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

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

دعم المتصفح

  • 55
  • 15
  • 52
  • 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!';
}

يستغرق اكتمال العملية أعلاه 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 هذه المهمة نيابةً عنك، إليك مثال من خلال REPL في Babel.

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

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، ويسعدني أن أراها يتم تطبيقها، في الواقع، في المتصفحات. عذرًا!