הרצת callback רק עם התוצאה של הקריאה האחרונה
-
יש פונקציה אסינכרונית איטית שנקראת על כל אירוע מסויים, בסיום הפוקנציה אני מעדכן את הממשק עם התוצאה, אני רוצה שכל קריאה נוספת לפונקציה תבטל את הקריאה הקודמת, כך שעדכון הממשק יקרה רק עם התוצאה של הקריאה האחרונה. (שימו לב, מדובר על התוצאה של הקריאה האחרונה - לא על התוצאה שחזרה אחרונה, זה יכול להיות מקריאה קודמת)
(הצורך הנ"ל הוא בנפרד מהצורך לעשות debouncing כדי שלא לקרוא לפונציה מליון פעמים, בנוסף ל-debouncing יש עדיין צורך להתעלם מקריאות קודמות כאשר יש קריאה חדשה)כתבתי פונקציה כזו:
// wraps a function so that only the last call returns a value, all previous calls return null function returnOnce (func) { let callCounter = 0; const wrapper = async (...args) => { const myId = ++callCounter; const result = await func(...args); if (myId < callCounter) { return null; } return result; }; return wrapper; }
ואז:
const doStuffReturnOnce = returnOnce(doStuff) function onKeyPress(key) { const result = await doStuffReturnOnce(key); if (result) { updateUI(result) } }
אני מרגיש שיש דרך יותר טובה לעשות את זה.
רעיונות מישהו? -
ביכולתך לממש Debouncing כך:
function returnOnce(func) { let counter = 0; return async (...args) => { const id = ++counter; await new Promise(x => setTimeout(x, 300)); if (counter > id) return; const result = await func(...args); if (counter === id) { return result } }; }
Reactive programming
אישית הייתי משתמש בRxJs עבור המשימה, הרבה יותר אפשרויות, פחות קוד לתחזוק,
fromEvent(input, 'keyup') .pipe( debounceTime(300), distinctUntilChanged(), switchMap(e => func(e.target.value)) ) .subscribe(result => updateUi(result))
בדוגמא למעלה אנו יוצרים Observable שמאזין לאירוע
keyup
של התבת טקסט,
לאחר מכן אנחנו מוסיפים אופרטורים (באמצעותם ניתן לקבוע מה יתרחש בשעה שערך יוחזר) באמצעות המתודה Pipe:-
DebounceTime
עבור כל ערך שחוזר, המתן X מילי שניות, במידה ובמהלך הזמן הזה ערך חדש מופיע, התעלם מהערך הנוכחי, וחזור על התהליך שוב, במידה והזמן חלף ללא הופעת ערך חדש, העבר את הערך לאופרטור הבא. -
DistinctUntilChanged
התעלם מהערך הנוכחי במידה והוא זהה לערך הקודם, במידה ולא, העבר את הערך לאופרטור הבא. -
SwitchMap
האופרטור מקבל מתודה שמקבלת את הערך שחזר מהאופרטור הקודם כפרמטר ומחזירה Observable/Promise, המתודה תרוץ עבור כל ערך שחוזר, הObservable/Promise הפנימיים יתבטלו במידה והם עדיין רצים בשעה שערך חדש מופיע (עבור Promise אין לדבר משמעות מלבד התעלמות מהתוצאה).
Promise vs Observable
- Observable אינו מוגבל להחזרת ערך בודד.
- Promise רץ ברגע שהוא נוצר, לעומת Observable שירוץ רק במידה ונרשם אליו באמצעות Subscribe (הדבר אינו נכון עבור Hot Observable אולם השימוש בו נדיר יותר ונתעלם ממנו לצורך העניין).
לסיכום
אני משוכנע שידע בתכנות ריאקטיבי, הופך אותך למתכנת טוב יותר, במיוחד בפיתוח ממשקים, בו יש צורך לטפל בstream של אירועים בצורה אסינכרונית.
-
-
@רפאל
א) תודה רבה על התשובה מאירת העיניים. עדיין נשאר לי הרבה שיעורי בית עד שאלמד טוב את הספרייה, אבל זה בהחלט מרחיב את הדעת.
ב) עשיתי crash course מהיר על rxjs, ולפי מיעוט הבנתי לבינתיים, הקוד שהבאת לא עושה בדיוק מה שביקשתי. אני רוצה לקבל את התוצאה של הקריאה האחרונה. לא התוצאה שחזרה אחרונה. -
@רפאל נעלבתי... (רק טיפה ) נראה לך שהערתי בלי לקרוא את התיעוד? אני מבין שטעיתי, ורק עכשיו אני מתחיל להבין איפה הטעות שלי.
אני הבנתי שה-map מחזיר observable (פנימי) חדש בצורה אסינכרונית אחרי שהקריאה לשרת חוזרת. עכשיו אני מבין שפרומיס יכול להיות במקום observable והוא מחזיר מיד את הפרומיס בצורה סינכרונית. זה נכון? -
@yossiz אמר בהרצת callback רק עם התוצאה של הקריאה האחרונה:
נעלבתי
גם לי לא לקח יום אחד להבין איך הכל עובד, ואני בטוח שלך יקח פחות.
@yossiz אמר בהרצת callback רק עם התוצאה של הקריאה האחרונה:
והוא מחזיר מיד את הפרומיס בצורה סינכרונית
בהחלט, אבל שים לב להבדלים בין:
- Map מיועד למיפוי ערך פשוט
- SwitchMap יבטל את הרישום לObservable הפנימי הקיים וייצור Observable עבור הערך החדש
- MergeMap ייצר Observable פנימי עבור הערך החדש, בלי לבטל את הObservable הפנימי, כך שאין סוף Observables פנימיים יכולים להיות פעילים במקביל (אפשר להגביל את הכמות).
- ExhaustMap יתעלם מערכים חדשים כל עוד שהObservable הפנימי פעיל.
- ConcatMap ימתין שהObservable הפנימי יושלם לפני שיצור Observable פנימי נוסף, הערכים נשמרים בBuffer פנימי.
להלן יישום קטנטן שמציג Countdown כתוצאה להזנה לתוך תבת טקסט, ובסיום מציג מערך המכיל את האותיות המרכיבות את הטקסט, הקוד מראה בבירור שכל פעם שמגיע ערך חדש, הObservable הפנימי הקודם יתבטל והטיימר יתאפס.
תרשים ציר זמן המתאר את ההבדלים בין הMaps השונים:
- $Source מתאר את הstream המקורי.
הכחול מייצג את הערך הראשון מהstream החיצוני
הצהוב את השני
הירוק את השלישי - $Target מייצג את הstream הפנימי (כל איבר מהstream החיצוני ימופה לstream פנימי)
באיור הבא כל איבר בstream המקורי "משתכפל" כמספר הערכים שהstream הפנימי פלט (בכפוף למדיניות של האופרטורים הנ"ל):