גנרטורים הם אחד מהפיצ'רים המעניינים, המסקרנים והחדשניים שיש ב-ECMAScript 6 והופכים ליותר מעניינים ככל שמשתמשים בשפה לדברים שדורשים זמן ויכולות כבדות. הפיצ'ר הזה יכול להיות מבלבל – אז אלו מכם שרגילים לגלול ולראות רק את הדוגמאות, עדיף שהיום לא תעשו את זה ותטרחו לקרוא את ההסברים שלי כיוון שזה נושא שהוא קצת קשה לתפיסה בהתחלה.
בואו ונסתכל על דוגמת הקוד הזו, שנכתבה על ידי קייל סימפסון ולקחתי מהאתר של דיוויד וולש שמסביר את עניין הגנרטורים באופן נהדר, אבל באנגלית.
setTimeout(function(){
console.log("Hello World");
},1);
function foo() {
// NOTE: don't ever do crazy long-running loops like this
for (var i=0; i< =1E10; i++) {
console.log(i);
}
}
foo();
// 0..1E10
// "Hello World"
מה קורה פה? יש לי קריאה לפונקציה, שמחכה מילישניה שלמה ואז מדפיסה מחרוזת. אבל בפועל, המחרוזת תודפס הרבה, הרבה אחרי הלולאה הארוכה והרבה אחרי מילישניה. למה? כי כאשר ההדפסה רוצה להתרחש, יש לנו לולאה שרצה. אי אפשר להפריע ללולאה והיא תרוץ עד הסיום. זו אחת המוסכמות הגדולות בתחום התכנות. לפחות עד עכשיו.
הגנרטורים ב-ECMAScript 6 מאפשרים לנו ליצור פונקציות/לולאות או קטעי קוד אחרים ש'אפשר להפריע להם'. הפונקציה עוצרת, במקום שבו אנו בוחרים כמובן ומחזירה תוצאה. הקוד שמחוץ לפונקציה יכול לקבל את התוצאה ולהתניע מחדש את הפונקציה.
נשמע מסובך? ממש ממש לא. פונקציות כאלו, שיכולות לעצור את עצמן ואז להמשיך נקראות 'גנרטורים', הן נראות ממש כמו פונקציות רגילות, רק שיש להן כוכבית לפני השם. פקודת העצירה נקראת yield. ברגע שפונקציה קוראת ל-yield, קורים שני דברים – הראשון הוא שה-yield מחזיר ערך והדבר השני הוא שהפונקציה עוצרת עד שמשהו חיצוני ממשיך אותה.
כלומר, הפונקציה עוצרת רק כשהיא רוצה לעצור, אף אחד לא יכול לעצור את הגנרטור מבחץ. היא יכולה להתניע מחדש רק כשמשהו מבחוץ מתניע אותה. היא לא יכולה להתניע את עצמה מחדש. ההתנעה הראשונית של הפונקציה וההתנעות הנוספות בעקבות כל yield נעשות באמצעות next.
נשמע מסובך? חבל על כאב הראש, הנה הדוגמה:
function *myGenerator() {
var x = 'start';
console.log(x);
yield 'yieldResult';
x = 'end';
console.log(x);
}
מה הולך פה? יש לנו את שם הפונקציה, foo, הפונקציה נראית כמו כל פונקציה רגילה, אבל שימו לב למשהו מאוד מעניין – יש בשם שלה כוכבית, מה שמראה על כך שהיא לא פונקציה פושטית אלא גנרטור. ובתוכה יש yield. לפונקציות גנרטור, קוראים עם next כמו הדוגמה שלעיל:
var it = myGenerator();
it.next();
מה לפי דעתכם יודפס? אתם מוזמנים להדביק את זה בקונסולה ולראות או להאמין לי כשאני אומר שמה שנראה בקונסולה זה 'start' למה הפונקציה לא ממשיכה כרגיל? כי יש לנו yield שעוצר את הכל! רוצים להמשיך? אין בעיה! משתמשים שוב ב-next!
function *myGenerator() {
var x = 'start';
console.log(x);
yield 'yieldResult';
x = 'end';
console.log(x);
}
var it = myGenerator();
it.next(); //start
it.next(); //end
רגע, שימו לב שגם ה-yield מחזיר לנו משהו. איך אנחנו מקבלים את הנתון הזה? בקלות רבה. אם נחזיר את it.next לתוך משתנה, אנו נראה שהוא מורכב מאובייקט שיש לו result, שהוא מה שה-yield מעביר לנו ו-status, שמספר על האם הגנרטור סיים את פעולתו או לא:
var resultA = it.next(); //start
console.log(resultA); //Object { done: false, value: "yieldResult"}
var resultB = it.next(); //end
console.log(resultB); //Object { done: true, value: undefined}
See the Pen ES6 Generators by Ran Bar-Zik (@barzik) on CodePen.
אני גם יכול לאפשר תקשורת דו כיוונית – כלומר בהתנעה לשגר ערך ל-yield שהגנרטור ישתמש בו. הנה, שימו לב לדוגמה הבאה:
function *myGenerator() {
var a = yield 'yieldResult';
console.log(a);
}
var it = myGenerator();
it.next(); //Initial start
it.next(6); //end, the console will be 6
מה קורה פה? ה-yield, שעוצר את הגרנטור, גם מחזיר ערך ל-a. איזה ערך? כל ערך שההתנעה השניה מעבירה לו (ההתנעה הראשונה מפעילה את הגנרטור, ההתנעה השניה מפעילה אותו אחרי ה-yield הראשון). כיוון שבהתנעה הראשונה אני מעביר 6, אני גם אקבל אותו בתוך הגנרטור. כלומר ה-yield יכול גם להחזיר וגם לקבל ערך והוא המנגנון היחידי העומד לרשותי בכל מה שקשור להעברת מידע מ/אל הגרנטור.
הגרנטור יכול להחזיר ערך באמצעות return, אבל עדיף שלא לעשות את זה, כיוון שבלולאות for of הערך האחרון באיטרציה לא יוחזר. רגע, מה? מה זו לולאות for of? איך היא קשורה לגנרטורים? ואיך כל זה קשור לאהבת אמת? על זאת ועוד (חוץ מהקטע של אהבת אמת) במאמר הבא.
4 תגובות
"כיוון שבהתנעה הראשונה אני מעביר 6". לא אמור להיות כתוב "בהתנעה השנייה"?
תודה רן!
תודה.
לפני תקופה קצרה, ניסיתי לחפש משהו בין מאות עמודי אינטרנט (עם id עוקב), אבל לא הצלחתי (ברמת השקעה סבירה, למי שJS לא השפה שלו) לגרום ללולאה לחכות לטעינת הדף וחיפוש הטקסט. נשמע שזה הפיתרון הפשוט.
הפקודות האלו והסינטקס בעצם לא מוכרים אם אין לי ES6?
לא, אתה מחפש פונקציה אסינכרונית (promises) שהסברתי עליהן במאמר אחר בסדרה.