תהליך אסינכרוני בתוך לולאת JavaScript


אני מריץ לולאת אירועים מהצורה הבאה:

var i;
var j = 10;
for (i = 0; i < j; i++) {

    asynchronousProcess(callbackFunction() {
        alert(i);
    });
}

אני מנסה להציג סדרת התראות שמראות את המספרים מ-0 עד 10. הבעיה היא שברגע שהפונקציה הקריאה חוזרת נפעלת, הלולאה כבר עברה כמה איטרציות והיא מציגה ערך גבוה יותר של i. יש המלצות איך לתקן את זה?

תשובה #1

לולאת for רצה מיד עד סיום בעוד שכל הפעולות האסינכרוניות שלך מתחילות. כאשר הן יסתיימו בזמן מסוים בעתיד ויקראו לפונקציות הקריאה החוזרות שלהן, הערך של משתנה האינדקס של הלולאה שלך i יהיה בערכו האחרון עבור כל פונקציות הקריאה החוזרות.

זה בגלל שהלולאת for אינה מחכה לסיום פעולה אסינכרונית לפני שהיא ממשיכה לאיטרציה הבאה של הלולאה וכי הקריאות חזרה האסינכרוניות נקראות בזמן מסוים בעתיד. לכן, הלולאה משלימה את האיטרציות שלה ואז הקריאות חזרה נקראות כאשר הפעולות האסינכרוניות מסתיימות. כתוצאה מכך, המדד של הלולאה "סיים" ונמצא בערכו הסופי עבור כל הקריאות חזרה.

כדי לעקוף זאת, עליך לשמור באופן ייחודי את אינדקס הלולאה בנפרד עבור כל callback. ב-Javascript, הדרך לעשות זאת היא ללכוד אותו ב-closure של פונקציה. זה יכול להיעשות או על ידי יצירת פונקציית closure פנימית במיוחד למטרה זו (דוגמה ראשונה מוצגת למטה) או על ידי יצירת פונקציה חיצונית שאליה מעבירים את האינדקס ומאפשרים לה לשמור על האינדקס באופן ייחודי עבורך (דוגמה שנייה מוצגת למטה).

נכון לשנת 2016, אם יש לך מימוש ES6 מלא של JavaScript העומד במפרט, אתה יכול גם להשתמש ב-let כדי להגדיר את משתנה הלולאת for והוא יוגדר באופן ייחודי עבור כל איטרציה של לולאת ה-for (המימוש השלישי למטה). עם זאת, שים לב שזו תכונה שהתווספה במועד מאוחר במימושי ES6 ולכן עליך לוודא שסביבת ההרצה שלך תומכת באפשרות זו.

השתמש ב-.forEach() כדי לבצע איטרציה כיוון שהיא יוצרת סגירת פונקציה משלה

someArray.forEach(function(item, i) {
    asynchronousProcess(function(item) {
        console.log(i);
    });
});

צור סגירת פונקציה משלך באמצעות IIFE

var j = 10;
for (var i = 0; i < j; i++) {
    (function(cntr) {
        // here the value of i was passed into as the argument cntr
        // and will be captured in this function closure so each
        // iteration of the loop can have it's own value
        asynchronousProcess(function() {
            console.log(cntr);
        });
    })(i);
}

צור או עדכן פונקציה חיצונית והעבר לה את המשתנה

אם אתה יכול לשנות את הפונקציה asynchronousProcess(), אז אתה יכול פשוט להעביר את הערך לשם ולגרום לפונקציה asynchronousProcess() להחזיר את cntr חזרה אל הקריאה חזרה ככה:

var j = 10;
for (var i = 0; i < j; i++) {
    asynchronousProcess(i, function(cntr) {
        console.log(cntr);
    });
}

השתמש ב-ES6 let

אם יש לך סביבה להרצת Javascript שתומכת במלואה ב-ES6, אתה יכול להשתמש ב-let בלולאת ה-for שלך כך:

const j = 10;
for (let i = 0; i < j; i++) {
    asynchronousProcess(function() {
        console.log(i);
    });
}

הגדרת let בהצהרת לולאת for כמו זו תיצור ערך ייחודי של i עבור כל קריאה של הלולאה (שזה מה שאתה רוצה).

סיריאליזציה עם הבטחות ו-async/await

אם הפונקציה האסינכרונית שלך מחזירה הבטחה, ואתה רוצה לסדר את הפעולות האסינכרוניות שלך כך שירוצו אחת אחרי השנייה במקום במקביל, ואתה מריץ בסביבה מודרנית שתומכת ב-async ו-await, אז יש לך יותר אפשרויות.

async function someFunction() {
    const j = 10;
    for (let i = 0; i < j; i++) {
        // wait for the promise to resolve before advancing the for loop
        await asynchronousProcess();
        console.log(i);
    }
}

זה יוודא שרק קריאה אחת ל-asynchronousProcess() מתבצעת בזמן נתון והלולאת for לא תתקדם עד שכל אחת מסתיימת. זה שונה מהסכימות הקודמות שריצו את כל הפעולות האסינכרוניות שלך במקביל ולכן זה תלוי לחלוטין באיזה עיצוב אתה רוצה. הערה: await עובד עם הבטחה (promise) כך שהפונקציה שלך חייבת להחזיר הבטחה שתיפתר או תידחה כשהפעולה האסינכרונית מסתיימת. בנוסף, שים לב שכדי להשתמש ב-await הפונקציה המכילה חייבת להיות מוגדרת כ-async.

הרץ פעולות אסינכרוניות במקביל והשתמש ב-Promise.all() כדי לאסוף תוצאות לפי הסדר

 function someFunction() {
     let promises = [];
     for (let i = 0; i < 10; i++) {
          promises.push(asynchonousProcessThatReturnsPromise());
     }
     return Promise.all(promises);
 }

 someFunction().then(results => {
     // array of results in order here
     console.log(results);
 }).catch(err => {
     console.log(err);
 });

תשובה #2

async await כאן (ES7), אז עכשיו אפשר לעשות את הדברים האלו בקלות מאוד.

  var i;
  var j = 10;
  for (i = 0; i < j; i++) {
    await asycronouseProcess();
    alert(i);
  }

זכור, זה עובד רק אם asycronouseProcess מחזיר Promise

אם asycronouseProcess לא נמצא בשליטתך, אז אתה יכול לגרום לו להחזיר Promise בעצמך כך

function asyncProcess() {
  return new Promise((resolve, reject) => {
    asycronouseProcess(()=>{
      resolve();
    })
  })
}

לאחר מכן החלף שורה זו await asycronouseProcess(); ב- await asyncProcess();

הבנת Promises לפני שמתעמקים ב-async await היא חיונית (כדאי גם לקרוא על תמיכה ב-async await)

תשובה #3

יש לך המלצה איך לתקן את זה?

מספר. אתה יכול להשתמש ב-bind:

for (i = 0; i < j; i++) {
    asycronouseProcess(function (i) {
        alert(i);
    }.bind(null, i));
}

או, אם הדפדפן שלך תומך ב-let (זה יהיה בגרסת ECMAScript הבאה, אולם Firefox כבר תומך בזה כבר זמן מה) אתה יכול היה לקבל:

for (i = 0; i < j; i++) {
    let k = i;
    asycronouseProcess(function() {
        alert(k);
    });
}

או, אתה יכול לבצע את העבודה של bind ידנית (במקרה שהדפדפן לא תומך בזה, אבל אני הייתי אומר שאתה יכול לממש שכבת ביניים במקרה כזה, זה אמור להיות בלינק למעלה):

for (i = 0; i < j; i++) {
    asycronouseProcess(function(i) {
        return function () {
            alert(i)
        }
    }(i));
}

בדרך כלל אני מעדיף את let כשאני יכול להשתמש בו (למשל עבור תוספי Firefox); אחרת bind או פונקציית currying מותאמת אישית (שלא דורשת אובייקט הקשר).