تسمح لك الدوال غير المتزامنة بكتابة رمز برمجي يستند إلى الوعد كما لو كان متزامنًا.
يتم تفعيل الدوالّ غير المتزامنة تلقائيًا في Chrome وEdge وFirefox وSafari، وهي رائعة جدًا. وتسمح لك بكتابة رمز برمجي يستند إلى الوعود كما هو الحال مع الرموز البرمجية المتزامنة، ولكن بدون حظر سلسلة التعليمات الرئيسية. وتجعل هذه الوظائف تعليماتك البرمجية غير المتزامنة أقل "ذكاءً" وأكثر سهولة في القراءة.
تعمل الدوالّ غير المتزامنة على النحو التالي:
async function myFirstAsyncFunction() {
try {
const fulfilledValue = await promise;
} catch (rejectedValue) {
// …
}
}
إذا استخدمت الكلمة الرئيسية async
قبل تعريف دالة، يمكنك بعد ذلك استخدام
await
داخل الدالة. عند await
وعد، يتم إيقاف الوظيفة مؤقتًا
بطريقة غير حظر إلى أن يتم حلّ الوعد. وفي حال استيفاء الوعد،
ستستردّ القيمة. إذا رفض الوعد، يتم طرح القيمة المرفوضة.
دعم المتصفح
مثال: تسجيل عملية استرجاع
لنفترض أنّك تريد جلب عنوان 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-loop موثوق بها ومملة. أفضل بكثير. في المستقبل، ستحصل على
مكررات غير متزامنة،
التي بدورها
ستستبدل حلقة 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); } }
حلّ بديل لتوفير إمكانية استخدام المتصفّحات: أدوات إنشاء
إذا كنت تستهدِف المتصفّحات التي تتيح استخدام المولدات (بما في ذلك أحدث إصدار من كل متصفّح رئيسي )، يمكنك استخدام polyfill لوظائف غير المتزامنة.
سيتولى Babel تنفيذ ذلك نيابةً عنك، في ما يلي مثال على ذلك من خلال Babel REPL
- يُرجى ملاحظة مدى تشابه الرمز البرمجي الذي تم تحويله. يُعدّ هذا التحويل جزءًا من الإعداد المُسبَق es2017 في Babel.
أنصح باستخدام نهج التحويل البرمجي، لأنّه يمكنك إيقافه بعد أن يصبح المتصفّحات المستهدَفة متوافقة مع الوظائف غير المتزامنة، ولكن إذا كنت حقًا لا تريد استخدام أداة تحويل برمجي، يمكنك استخدام البوليفيلر من 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، و يسرّنا أن نرى أنّها أصبحت متاحة في المتصفّحات. رائع.