ES2018 – איטרציה אסינכרונית

תוספת משמעותית לתקן ES2018 שמאפשרת לנו להריץ לולאות באופן אסינכרוני. כן כן.

אחד מהפיצ'רים המעניינים שיש ב-ES2018 ונכנס ממש עכשיו הוא Asynchronous Iterations. על מנת להבין אותו אנחנו צריכים לצלול קצת לעולם המופלא של איטרטורים. אני מבטיח שזה יהיה נחמד וכיפי.

איך עושים איטרציה? טוב, כל מתכנת ג'אווהסקריפט יודע – לוקחים מערך ועוברים עליו.


const a = [1, 2, 3];
for(let i in a) {
  console.log(i); // 0 1 2
}

אבל מה קורה מאחורי הקלעים? מאחורי הקלעים יש למערך פונקצית איטרציה שרצה בכל פעם שאנחנו מריצים for of או כל פונקצית איטרציה אחרת (כמו forEach למשל). בעצם, מה שאנחנו רואים למעלה זה בעצם:


const a = [1, 2, 3];
const iteratorOfA = a[Symbol.iterator]();
let result;

result = iteratorOfA.next(); 
console.log(result); // { value: 1, done: false }
result = iteratorOfA.next(); 
console.log(result); // { value: 2, done: false }
result = iteratorOfA.next(); 
console.log(result); // { value: 3, done: false }
result = iteratorOfA.next(); 
console.log(result); // { value: undefined, done: true}

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


const myObj = {
  key1: 'val1',
  key2: 'val2',
}
for(let i of myObj) {
  console.log(i);
}

מה אני אקבל? שגיאה מסוג myObj is not iterable. למה? כי פונקצית for of משמשת למערכים בלבד! אפילו כתבתי על זה. אבל אני יכול להוסיף פונקצית איטרציה לאובייקט וכך לגרום ל for of לעבוד איתה! איך? באמצעות Symbol.iterator שמקבל פונקציה (שימו לב לא להשתמש פה בפונקצית חץ) שהיא עושה את המיון! זה הכל!


const myObj = {
  key1: 'val1',
  key2: 'val2',
  [Symbol.iterator]: function() {
    const keys = Object.keys(this);
    let i = 0;
    return {
      next: () => {
        if (i == keys.length) return {value: null, done: true};
        return {
          value: [keys[i], this[keys[i++]]],
          done: false
        };
      }
    }
  }
}

for(let i of myObj) {
  console.log(i); // ["key1", "val1"], ["key2", "val2"]
}

רוצים לשחק? זה ממש נחמד! רק שימו לב היטב תמיד להעלות את i כי אחרת נכנסים ללולאה אינסופית. בגדול אנחנו חייבים תמיד להקפיד להחזיר מתישהו done:true וזה הכל.

See the Pen iteration protocols example by Ran Bar-Zik (@barzik) on CodePen.

אפשר ממש להתפרע. למשל להחליט שבכל איטרציה יהיה גם כתוב Ran the king.


const myObj = {
  key1: 'val1',
  key2: 'val2',
  [Symbol.iterator]: function() {
    const keys = Object.keys(this);
    let i = 0;
    return {
      next: () => {
        if (i == keys.length) return {value: null, done: true};
        
        const result =  {
          value: this[keys[i]] + ' Ran The King',
          done: false
        };
        i++;
        return result;
      }
    }
  }
}

for(let i of myObj) {
  console.log(i); // val1 Ran The King, val2 Ran The King
}

See the Pen iteration protocols example 2 by Ran Bar-Zik (@barzik) on CodePen.

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

זה הזמן לחזור על: promise ועל async-await.

נשמע מסובך? זה לא. זה בדיוק כמו איטרטורים רגילים, רק שבמקום להחזיר ערך אנחנו מחזירים promise!


async function fetchAsync (i) {
   let response = await fetch('https://icanhazdadjoke.com/', {headers: {'Accept': 'application/json'}});
   let data = await response.json();
   return { value: `This is joke number ${i}: ${data.joke}`, done: false };
 }

let asyncIterable = {
  a: 1,
  b: 2,
  c: 3,
  d: 4,
  [Symbol.asyncIterator]: function() {
    const keys = Object.keys(this);
    let i = 0;
    return {
      next: () => {
        if (i == keys.length) return Promise.resolve({value: null, done: true});
		i++;
        return fetchAsync(i);
      }
    }
  }
};

async function process() { 
  for await (let item of asyncIterable) console.log(item);
}

process();

מסובך? ממש לא. בואו ונעבור על זה: יש לנו את פונקצית fetchAsync שהיא פונקציה אסינכרונית רגילה שמקבלת משתנה i ומחזירה בדיחת אבא מ-API + המספר הזה. פשוט, לא? אפשר להפעיל אותה ככה:


async function fetchAsync(i) {
   let response = await fetch('https://icanhazdadjoke.com/', {headers: {'Accept': 'application/json'}});
   let data = await response.json();
   return { value: `This is joke number ${i}: ${data.joke}`, done: false };
 }

fetchAsync(1).then((result) => {
  console.log(result.value);
})

מה מיוחד בה? היא מחזירה promise. למה? כי אנחנו קוראים ל-API באמצעות fetch – קריאה לשרת חיצוני עדיף שתהיה אסינכרונית כי אנחנו רוצים שהסקריפט ימשיך הלאה. זו הסיבה שבגללה אנחנו משתמשים ב-promise. אני משתמש בסינטקסט של async כדי שאוכל להשתמש ב-await בתוך הפונקציה. זה הכל.

עכשיו, נניח ויש לי אובייקט עם מספרים:


let asyncIterable = {
  key1: 1,
  key2: 2,
  key3: 3,
  key4: 4,
}

ואני רוצה לעבור עליו ולשלוח את המפתחות שלו לפונקציה. אני אהיה בבעיה. למה? כי קודם כל זה אובייקט. אני צריך לעבור עליו. אם אני רוצה להשתמש בפונקצית איטרציה כמו בתחילת המאמר. אבל אז אני נכנס לעוד בעיה – כי פונקצית האיטרציה שאני רוצה להשתמש בה היא fetchAsync והיא פונקציה אסינכרונית כי יש בה בקשה ל-API. אם אני אשתמש ב-Symbol.iterator רגיל או לולאה רגילה זה יהיה מעולה, אבל אני לא אוכל לקבל את התוצאות מהשרת.

אני צריך בעצם פונקצית איטרציה שיודעת להתמודד עם promises וזה בדיוק מה שהתקן החדש מספק לי: Symbol.asyncIterator שכל ההבדל בינו לבין Symbol.iterator הוא שהוא יודע להחזיר promises ולא ערכים רגילים. על מנת להשתמש ב- Symbol.asyncIterator אני צריך להשתמש בלולאה מיוחדת שיש בה await.


	for await (let item of asyncIterable) {
		console.log(item);
	}

בגלל שיש בה await היא חייבת להיות בתוך פונקציה אסינכרונית:


async function process() { 
	for await (let item of asyncIterable) {
		console.log(item);
	}
}

אם אתם רוצים דוגמה נוספת, פחות מעשית אבל יותר פשוטה – הנה קוד שמחזיר promises בלבד:


const myObj = {
  key1: 1,
  key2: 2,
  key3: 3,
  [Symbol.asyncIterator]: function() {
    const keys = Object.keys(this);
    let i = 0;
    return {
      next: () => {
		let result;
        if (i == keys.length) {
			result = {value: null, done: true};;
		} else {
		result = {
          value: [keys[i], this[keys[i]]],
          done: false
        }
		}
		i++;
        return Promise.resolve(result);
      }
    }
  }
};

async function runIt() { 
  for await (let item of myObj) {
	  console.log(item);
  }
}

runIt();

נשאלת בוודאי השאלה – למה לא להשתמש ב Promise.all או בשטיקים דומים? התשובה היא שהתוספת החשובה הזו לתקן מאפשרת לנו להשתמש בשיטת כתיבה סינכרונית כאשר אנו כותבים באופן אסינכרוני וזה הכיוון שג'אווהסקריפט הולכת אליו. הצעד הראשון היה async-await שנכנס ב-ES2017 – זה הצעד השני שמפזר קצת 'אבק קסמים' של async-await מעל ה-promises.

פוסטים נוספים שכדאי לקרוא

בינה מלאכותית

להריץ ממשק של open-webui על הרספברי פיי

להפעיל ממשק של צ׳אט ג׳יפיטי שאפשר לגשת אליו מכל מחשב ברשת הביתית על רספברי פיי עם מודל בשם tinydolphin שרץ על רספברי פיי.

גלילה לראש העמוד