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

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

جيك أرشيبالد
جيك أرشيبالد

يتم تفعيل الوظائف غير المتزامنة تلقائيًا في 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 بحلقة تكرارية، ما يجعلها أكثر تنظيمًا.

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

لقد أوضحت لك 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 بوحدة تخزين عادية ذات طابع مملّ وقابل للقراءة.

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

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

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

أنصحك باتّباع منهج تحويل الترجمة لأنّه يمكنك إيقافه بعد أن تتوافق المتصفحات المستهدفة مع الدوال غير المتزامنة، ولكن إذا كنت حقًا لا تريد استخدام برنامج تحويل الترجمة، يمكنك اختيار رمز 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 المسبق.

الناتج ليس بهذا جمالاً، لذلك انتبه لـcode-bloat.

عدم مزامنة كل الأشياء!

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

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