جافا سكريبت غير متزامن وانتظر في الحلقات

الأساسية asyncو awaitبسيط. تصبح الأمور أكثر تعقيدًا عند محاولة استخدام awaitالحلقات.

في هذه المقالة ، أرغب في مشاركة بعض المشاكل التي يجب الانتباه إليها إذا كنت تنوي استخدامها awaitفي الحلقات.

قبل ان تبدأ

سأفترض أنك تعرف كيفية استخدام asyncو await. إذا لم تفعل ذلك ، فاقرأ المقالة السابقة لتتعرف على نفسك قبل المتابعة.

تحضير مثال

بالنسبة لهذه المقالة ، لنفترض أنك تريد الحصول على عدد الفاكهة من سلة فواكه.

const fruitBasket = { apple: 27, grape: 0, pear: 14 };

تريد الحصول على رقم كل فاكهة من سلة الفاكهة. للحصول على رقم فاكهة ، يمكنك استخدام getNumFruitدالة.

const getNumFruit = fruit => { return fruitBasket[fruit]; }; const numApples = getNumFruit(“apple”); console.log(numApples); // 27

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

const sleep = ms => { return new Promise(resolve => setTimeout(resolve, ms)); }; const getNumFruit = fruit => { return sleep(1000).then(v => fruitBasket[fruit]); }; getNumFruit(“apple”).then(num => console.log(num)); // 27

وأخيرا، دعونا نقول لكم تريد استخدامها awaitو getNumFruitالحصول على عدد كل الفاكهة في وظيفة غير متزامن.

const control = async _ => { console.log(“Start”); const numApples = await getNumFruit(“apple”); console.log(numApples); const numGrapes = await getNumFruit(“grape”); console.log(numGrapes); const numPears = await getNumFruit(“pear”); console.log(numPears); console.log(“End”); };

مع هذا ، يمكننا البدء في البحث awaitفي الحلقات.

انتظر في حلقة for

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

const fruitsToGet = [“apple”, “grape”, “pear”];

سنقوم بحلقة من خلال هذه المجموعة.

const forLoop = async _ => { console.log(“Start”); for (let index = 0; index < fruitsToGet.length; index++) { // Get num of each fruit } console.log(“End”); };

في حلقة for-loop ، سنستخدمها getNumFruitللحصول على عدد كل فاكهة. سنقوم أيضًا بتسجيل الرقم في وحدة التحكم.

منذ getNumFruitإرجاع الوعد ، يمكننا awaitحل القيمة قبل تسجيله.

const forLoop = async _ => { console.log(“Start”); for (let index = 0; index < fruitsToGet.length; index++) { const fruit = fruitsToGet[index]; const numFruit = await getNumFruit(fruit); console.log(numFruit); } console.log(“End”); };

عند الاستخدام await، تتوقع أن تتوقف JavaScript عن التنفيذ مؤقتًا حتى يتم حل الوعد المنتظر. هذا يعني awaitأنه يجب تنفيذ s في for-loop في سلسلة.

والنتيجة هي ما تتوقعه.

“Start”; “Apple: 27”; “Grape: 0”; “Pear: 14”; “End”;

يعمل هذا السلوك مع معظم الحلقات (مثل whileو for-ofالحلقات) ...

لكنها لن تعمل مع الحلقات التي تتطلب رد اتصال. ومن الأمثلة على هذه الحلقات التي تتطلب تراجع تشمل forEach، map، filter، و reduce. سوف نبحث في كيف awaitيؤثر forEach، mapو filterفي الفروع القليلة المقبلة.

انتظر في حلقة forEach

سنفعل نفس الشيء كما فعلنا في مثال الحلقة. أولاً ، دعنا ننتقل عبر مجموعة الفواكه.

const forEachLoop = _ => { console.log(“Start”); fruitsToGet.forEach(fruit => { // Send a promise for each fruit }); console.log(“End”); };

بعد ذلك ، سنحاول الحصول على عدد الثمار باستخدام getNumFruit. (لاحظ asyncالكلمة الأساسية في وظيفة رد الاتصال. نحتاج إلى هذه asyncالكلمة الأساسية لأنها awaitفي وظيفة رد الاتصال).

const forEachLoop = _ => { console.log(“Start”); fruitsToGet.forEach(async fruit => { const numFruit = await getNumFruit(fruit); console.log(numFruit); }); console.log(“End”); };

قد تتوقع أن تبدو وحدة التحكم بالشكل التالي:

“Start”; “27”; “0”; “14”; “End”;

لكن النتيجة الفعلية مختلفة. تستمر JavaScript في الاتصال console.log('End') قبل حل الوعود في حلقة forEach.

تسجل وحدة التحكم بهذا الترتيب:

‘Start’ ‘End’ ‘27’ ‘0’ ‘14’

يقوم JavaScript بهذا لأنه forEachلا يعي الوعد. لا يمكن أن تدعم asyncو await. أنت _لا يمكن_ استخدامها awaitفي forEach.

انتظر مع الخريطة

إذا كنت تستخدم awaitفي map، mapفستعود دائمًا بمجموعة من الوعد. هذا لأن الوظائف غير المتزامنة دائمًا ما تعيد الوعود.

const mapLoop = async _ => { console.log(“Start”); const numFruits = await fruitsToGet.map(async fruit => { const numFruit = await getNumFruit(fruit); return numFruit; }); console.log(numFruits); console.log(“End”); }; “Start”; “[Promise, Promise, Promise]”; “End”;

نظرًا لأن mapدائمًا ما تعيد الوعود (إذا كنت تستخدم await) ، عليك الانتظار حتى يتم حل مجموعة الوعود. يمكنك فعل هذا مع await Promise.all(arrayOfPromises).

const mapLoop = async _ => { console.log(“Start”); const promises = fruitsToGet.map(async fruit => { const numFruit = await getNumFruit(fruit); return numFruit; }); const numFruits = await Promise.all(promises); console.log(numFruits); console.log(“End”); };

إليك ما تحصل عليه:

“Start”; “[27, 0, 14]”; “End”;

يمكنك التلاعب بالقيمة التي ترجعها في وعودك إذا كنت ترغب في ذلك. ستكون القيم التي تم حلها هي القيم التي ترجعها.

const mapLoop = async _ => { // … const promises = fruitsToGet.map(async fruit => { const numFruit = await getNumFruit(fruit); // Adds onn fruits before returning return numFruit + 100; }); // … }; “Start”; “[127, 100, 114]”; “End”;

انتظر مع مرشح

عندما تستخدم filter، تريد تصفية مصفوفة بنتيجة محددة. لنفترض أنك تريد إنشاء مصفوفة تحتوي على أكثر من 20 فاكهة.

إذا كنت تستخدمه بشكل filterطبيعي (دون انتظار) ، فستستخدمه على النحو التالي:

// Filter if there’s no await const filterLoop = _ => { console.log(‘Start’) const moreThan20 = await fruitsToGet.filter(fruit => { const numFruit = fruitBasket[fruit] return numFruit > 20 }) console.log(moreThan20) console.log(‘End’) }

تتوقع moreThan20أن تحتوي على تفاح فقط لأن هناك 27 تفاحة ، لكن هناك صفر عنب و 14 كمثرى.

“Start”[“apple”]; (“End”);

awaitفي filterلا يعمل بنفس الطريقة. في الواقع ، لا يعمل على الإطلاق. تحصل على المجموعة غير المصفاة مرة أخرى ...

const filterLoop = _ => { console.log(‘Start’) const moreThan20 = await fruitsToGet.filter(async fruit => { const numFruit = getNumFruit(fruit) return numFruit > 20 }) console.log(moreThan20) console.log(‘End’) } “Start”[(“apple”, “grape”, “pear”)]; (“End”);

Here's why it happens.

When you use await in a filter callback, the callback always a promise. Since promises are always truthy, everything item in the array passes the filter. Writing await in a filter is like writing this code:

// Everything passes the filter… const filtered = array.filter(true);

There are three steps to use await and filter properly:

1. Use map to return an array promises

2. await the array of promises

3. filter the resolved values

const filterLoop = async _ => { console.log(“Start”); const promises = await fruitsToGet.map(fruit => getNumFruit(fruit)); const numFruits = await Promise.all(promises); const moreThan20 = fruitsToGet.filter((fruit, index) => { const numFruit = numFruits[index]; return numFruit > 20; }); console.log(moreThan20); console.log(“End”); }; Start[“apple”]; End;

Await with reduce

For this case, let's say you want to find out the total number of fruits in the fruitBastet. Normally, you can use reduce to loop through an array and sum the number up.

// Reduce if there’s no await const reduceLoop = _ => { console.log(“Start”); const sum = fruitsToGet.reduce((sum, fruit) => { const numFruit = fruitBasket[fruit]; return sum + numFruit; }, 0); console.log(sum); console.log(“End”); };

You'll get a total of 41 fruits. (27 + 0 + 14 = 41).

“Start”; “41”; “End”;

When you use await with reduce, the results get extremely messy.

// Reduce if we await getNumFruit const reduceLoop = async _ => { console.log(“Start”); const sum = await fruitsToGet.reduce(async (sum, fruit) => { const numFruit = await getNumFruit(fruit); return sum + numFruit; }, 0); console.log(sum); console.log(“End”); }; “Start”; “[object Promise]14”; “End”;

What?! [object Promise]14?!

Dissecting this is interesting.

  • In the first iteration, sum is 0. numFruit is 27 (the resolved value from getNumFruit(‘apple’)). 0 + 27 is 27.
  • In the second iteration, sum is a promise. (Why? Because asynchronous functions always return promises!) numFruit is 0. A promise cannot be added to an object normally, so the JavaScript converts it to [object Promise] string. [object Promise] + 0 is [object Promise]0
  • In the third iteration, sum is also a promise. numFruit is 14. [object Promise] + 14 is [object Promise]14.

Mystery solved!

This means, you can use await in a reduce callback, but you have to remember to await the accumulator first!

const reduceLoop = async _ => { console.log(“Start”); const sum = await fruitsToGet.reduce(async (promisedSum, fruit) => { const sum = await promisedSum; const numFruit = await getNumFruit(fruit); return sum + numFruit; }, 0); console.log(sum); console.log(“End”); }; “Start”; “41”; “End”;

But... as you can see from the gif, it takes pretty long to await everything. This happens because reduceLoop needs to wait for the promisedSum to be completed for each iteration.

There's a way to speed up the reduce loop. (I found out about this thanks to Tim Oxley. If you await getNumFruits() first before await promisedSum, the reduceLoop takes only one second to complete:

const reduceLoop = async _ => { console.log(“Start”); const sum = await fruitsToGet.reduce(async (promisedSum, fruit) => { // Heavy-lifting comes first. // This triggers all three getNumFruit promises before waiting for the next iteration of the loop. const numFruit = await getNumFruit(fruit); const sum = await promisedSum; return sum + numFruit; }, 0); console.log(sum); console.log(“End”); };

This works because reduce can fire all three getNumFruit promises before waiting for the next iteration of the loop. However, this method is slightly confusing since you have to be careful of the order you await things.

The simplest (and most efficient way) to use await in reduce is to:

1. Use map to return an array promises

2. await the array of promises

3. reduceحل القيم

const reduceLoop = async _ => { console.log(“Start”); const promises = fruitsToGet.map(getNumFruit); const numFruits = await Promise.all(promises); const sum = numFruits.reduce((sum, fruit) => sum + fruit); console.log(sum); console.log(“End”); };

هذا الإصدار سهل القراءة والفهم ، ويستغرق ثانية واحدة لحساب العدد الإجمالي للفواكه.

الماخذ الرئيسية

1. إذا كنت ترغب في تنفيذ awaitمكالمات متسلسلة ، فاستخدم for-loop(أو أي حلقة بدون رد اتصال).

2. لا تستخدم awaitمع forEach. استخدم for-loop(أو أي حلقة بدون رد) بدلاً من ذلك.

3. لا awaitداخل filterو reduce. دائمًا awaitمجموعة من الوعود مع map، إذن filterأو reduceوفقًا لذلك.

تم نشر هذه المقالة في الأصل على مدونتي .

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