איך עובד קוד אסינכרוני בJS?
-
לפני כשנה כשהייתי בPHP ניסתי ללמוד נוד
אבל לא הצלחתי לקלוט את הקטע של קוד אסינכרוני
עכשיו אחרי שהבנתי את הרעיון בGO אני מנסה לחזור ולהבין את נוד
אבל כנראה אני קשה הבנה , כי אני לא מתליח לקלוט את הנקודה.ברשותכם אני יביא קוד אסינכרוני בGO
ואתה תגידו לי איך זה נראה בנוד
ואולי אני יבין?בא נתחיל עם משהו בסיסי (לא מצאתי משהו יותר פשוט)
package main import ( "fmt" "time" ) func say(s string) { for i := 0; i < 5; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) } } func main() { go say("world") say("hello") }
(אני לא צריך קוד בJS שיעשה בדיוק אותו דבר
רק להבין את הרעיון) -
ראשית כל הקוד בJS הוא תמיד תמיד סינכרוני - כל שורה מתבצעת רק אחרי שזו שלפניה סיימה.
אין שום דרך לעקוף את ההתנהגות הזאת. אלא שישנם מתודות בודדות שבנויות באופן שהם "מסיימות" את פעולתם עוד לפני שהסתיימה תוצאת הפעולה המצופה מהם, וממילא הקוד ממשיך לשורה הבאה עוד לפני ש"קרה" הדבר.
משל קולע זה הדפסה למדפסת פיזית, זו פעולה שאורכת עשרות שניות בדרך כלל, ובזמן הזה ברור שהמחשב ממשיך לעשות עוד דברים (זה שונה מאוד מהמקרה שהראית בו המחשב עצמו עושה שני דברים, אין כזו חיה בJS).ישנם כאמור מתודות בודדות כאלו, בראשם setTimeout וsetInterval. יש גם את הXMLHttpRequest וגם הfetch ועוד כמה פונקציות בלתי מוכרות וזהו. בסביבת נוד יש עוד כמה וכמה מחלקות עם פונקציות כאלה בעיקר של IO, מחלקות כאלו בבסיסים כתובים בשפת C למיטב ידעתי.
לפונקציות אלו שנקראות non-blocking function (כלומר פונקציות שלא תוקעות את הקוד) יש אפשרות לתת callback שזה פונקציה שמתוזמנת לעת סיום הפעולה או חלק ממנה.אני מצטט מהמדריך המפורסם שלי מדריך כתיבת אפליקציה וובית, Full-Stack, צעד אחר צעד:
אולם בJavaScript כל פקודה שקשורה לקלט פלט (קבצים רשת ועוד) היא מסתיימת כביכול מיידית, בחינת "שגר ושכח", אבל עם callback - קרי "מה לעשות כשאני באמת אגמור". אם רוצים שהשורה שלאחריה תקרה רק בסיומה הרי שיש להעבירה לcallback ולא לשים אותה סתם שורה אחריה.
כל קוד אחר בJS הוא כן blocking כלומר לא זו בלבד שהשורה שאחריו ממתינה לסיומו המלא, אלא שבעת ובעונה אחת לא מבוצעים לעולם שתי פעולות בJS. זה אומר שאם יש לנו פונקציה עם לולאה, הקוד שלנו (גם בcallback שמחוץ להקשר הקוד) לא יעבוד בכלל עד ליציאה מהלולאה וסיום הפונקציה.
לסיכום: JS איננה אסינכרונית כלל אלא שבמעט המתודות שיש להם callback נעשה שימוש רב. הקוד שבcallback שלא מספיק ברור מתי הוא יקרה הוא זה שנתן את ערפל האסינכרוניות לשפה.
-
@nigun
לענות בדיוק על השאלה שלך, הקודהמקבילהכי קרוב בנוד הוא כך:const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); async function say (s) { for (let i = 0; i < 5; i++) { await sleep(100); console.log(s); } } say('world')); await say('hello');
אבל נראה לי שאתה מפספס פה נקודה חשובה. בנוד זה לא עובד כמו בגו. בגו יש לך אפשרות לכמה thread-ים מקבילים, ויש מושג של עצירת ה-thread עד להודעה הבא. בנוד יש לך רק thread אחד ואסור לעצור אותו בכל מחיר. לכן כל פונקציה שלא חוזרת מיד אלא מחכה לאירוע חייבת להתבצע בצורה אסינכרונית, לכן לא קיים בכלל דרך לקרוא ל-sleep בצורה סינכרונית.
-
@dovid אמר באיך עובד קוד אסינכרוני בJS?:
כל הקוד בJS הוא תמיד תמיד סינכרוני - כל שורה מתבצעת רק אחרי שזו שלפניה סיימה.
אין שום דרך לעקוף את ההתנהגות הזאת.כל קוד אחר בJS הוא כן blocking
כל הנ"ל נכון גם כלפי GO. כל הקוד הוא blocking (בקונטקסט של ה-thread הנוכחי) למעט מקרה מאוד מסויים אחד... קריאה לפונקציה על ידי מילת המפתח
go
.
לכן לא הבנתי לגמרי את הדרשה שלך...עריכה: אחרי מחשבה הבנתי את הנקודה שלך.
כל הקוד בJS הוא תמיד תמיד סינכרוני - כל שורה מתבצעת רק אחרי שזו שלפניה סיימה.
זה נכון גם בגו, אבל אתה מתכוון להגיד שב-JS אין מקביליות או אפילו מצג מדומה של מקביליות. משא"כ בגו אתה רואה את התוצאות של הקוד משולבות אחד עם השני כאילו הם קורים במקביל (וזה גם במקרה של thread פיזי אחד, כי ה-runtime יכול לעצור thread לוגי בכל מקום שנוח לו ולעבור ל-thread לוגי אחר בתוך אותו thread פיזי). אבל ב-JS עד שאתה מסיים פעולה, סביבת ההרצה לא יכולה לעבור לפעולה אחרת.
@nigun אם אתה רוצה להבין טוב את הנושא לך תלמד על ה-event loop ב-JS. דבר שלא קיים בגו.
-
@yossiz ההבהרה שלך לדברי אכן מדוייקת מצד עצמה, בזכות מילת מפתח אחת GO היא עולם אחר לחלוטין, ובJS אין את המילה הזו ואת ההשפעה שלה.
אבל אני בכלל לא השוואתי לGO, כתבתי א' ב' בJS כמו למישהו שלא יודע כלום, כי לדעתי בדיוק ככה יש לגשת לעניין. לא צריך לדעת GO (שלדעתי יש בפורום שלנו את היחיד בעולם שמכיר אותה ולא את JS) וגם לא event loop (זה מצויין לשיעורי העשרה, מרחיב אופקים, מעמיק הבנה, אבל לא נחוץ ואף מזיק לשלב זה של השאלה לדעתי). -
@yossiz בשביל להסביר את הקונטקסט שלי לגמרי שים לב שבבסיסה השאלה הייתה קיימת כש@nigun הכיר רק את PHP.
המשפט "כל קוד אחר בJS הוא כן blocking" מכוון בעיקר לשם, כלומר "זה בדיוק אותו דבר כמו PHP".
כמו"כ המילה "אחר" היא למעשה מטעה, כי זה כפי שאמרת כל ללא שום מיעוט. אלא שאחר נדרש פה בגלל שזה לאפוקי הפעולות שמבוצעות דה פקטו ע"י JS במסגרת המתודות האמורות לעיל, למשל הפעולות של הרשת במקרה של הXMLHttpRequest למשל. -
@nigun לא, זה לא דומה לexec. זה פשוט תזמון פעולות לאחרי שיקרה דברים.
קח דוגמא את הקוד הזה בPHP:<?php $website_page = file_get_contents('http://www.google.com/'); echo $website_page ; ?>
אם יהפכו את PHP להיות כמו JS אז הקוד ייראה ככה:
<?php file_get_contents_with_callback('http://www.example.com/', function ($website_page ) { echo $website_page; }); echo "Below the google page content: <br>"; ?>
-
@nigun אמר באיך עובד קוד אסינכרוני בJS?:
@dovid
ומה יקרה אם אני לא יכניס אותו לקולבאק?לא הבנתי. אם לא יהיה בקוד בקאלבק, אז לא יהיה קוד. עדיין הקובץ יירד אבל זה יהיה קצת לא שמיש...
ומה קורה אם אני רוצה לקבל ערכים (int) ממספר כתבות
ולעשות sum על כולם?אם אתה רוצה כמה כתובות ולעשות משהו מכולם יחד אתה אכן באתגר, כי הרי לכל אחד יש הקשר עצמאי לחלוטין.
בשביל אתגרים כאלה ואחרים עשו את Promise, אבל גם לפני Promise אתה יכול לחשוב על רעיונות פשוטים (למשל: אתה מכין משתנה לסיכום הערכים, ומשתנה נוסף שסופר כמה כתובות בוצעו. בכל קאלבק אתה גם מוסיף את התוצאה לסיכום וגם מפחית את הערך של המונה ב1, וגם בודק אם נשאר עוד כתובות להורדה. ברגע שהתשובה שלילית אתה יכול לבצע את הפעולה הסופית שרצית לעשות עם הסיכום הכולל).
היה מחלקה מפורסמת בשם async שעשתה הרבה דברים שקשורים לאתגרים הללו שמציב האופי הקאלבקי. כיום Promise פותר את רובם בוצרה קלה מאוד. -
@dovid
שוטטי קצת ברחבי הרשת להבין מה זה Promise
וזה מה שהבנתי:
הPromise שומר את הערך במשתנה בזיכרון (או אולי מערך)
ואז אפשר לגשת למשתנה הנ"ל מכל מקום בקוד ולבדוק את הערך.ואם אני מדגים את זה בגו
var x string go func(){ x = "foo" time.Sleep(200 * time.Millisecond) }() time.Sleep(100 * time.Millisecond) fmt.Println(x)
כאן אני יוצר את המשתנה X מכניס לו ערך
ושולף אותו בכל מקום שאני רוצה.אמנם Promise קצת יותר משוכלל
והוא מכיל סוג של מערך של משתנים, האם הפעולה בוצעה וכו'הבנתי נכון?
-
@nigun אמר באיך עובד קוד אסינכרוני בJS?:
הבנתי נכון?
לא.
פרומיסים באו לעולם כדי לתת תחביר יותר נוח ל-callback-ים השנואים.
לדעתי הסדר הנכון להבין טוב את הענין הוא כך:
א) תבין למה צריך callback-ים ב-JS.
ב) תבין למה אנשים מדברים בכזו שנאה כלפי callback-ים (או שזה כבר מובן מאליו...)
ג) תלמד איך פרומיסים פתרו את הבעיה.
ד) תלמד איך התחביר של async await מוסיף כמות עצומה של סוכר. -
@nigun אמר באיך עובד קוד אסינכרוני בJS?:
הבנתי נכון?
Promise בא לתת מענה על התנהגות אותה הגדרת "לא הבנתי"
ועוד לפני ששמענו אם הבנת את ההסבר
נדלקת על מושג חדש ששמעת
אז הנה אתרום גם את חלקי
יש גם await שזו שיכבה עליונה יותר
ויש עוד כמה שהאחרונה מהם הם הפיקסלים על המסך.. -
@yossiz
נתחיל ממה שהבנתי מה זה callback
בJS הדרך לנהל את התוצאה שמתקבל מבקשת HTTP (למשל)
היא לעשות פונקציה בתוך פונקציה
וכך אנחנו תופסים את התגובה, בלי שהיא "תברח" לנומכיוון שלפעמים צריך לכתוב כמה callback-ים אחד בתוך השני
זה מסבך את העניינים (גהנום בלע"ז).לכן הוסיפו את פרומיס ושות'
שינהלו את זה בצורה שונה.עכשיו הבנתי נכון? (אני מנחש שלא )
-
@nigun אמר באיך עובד קוד אסינכרוני בJS?:
לפני כשנה כשהייתי בPHP ניסתי ללמוד נוד
אבל לא הצלחתי לקלוט את הקטע של קוד אסינכרוני
עכשיו אחרי שהבנתי את הרעיון בGO אני מנסה לחזור ולהבין את נוד
אבל כנראה אני קשה הבנה , כי אני לא מתליח לקלוט את הנקודה.ברשותכם אני יביא קוד אסינכרוני בGO
ואתה תגידו לי איך זה נראה בנוד
ואולי אני יבין?בא נתחיל עם משהו בסיסי (לא מצאתי משהו יותר פשוט)
package main import ( "fmt" "time" ) func say(s string) { for i := 0; i < 5; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) } } func main() { go say("world") say("hello") }
(אני לא צריך קוד בJS שיעשה בדיוק אותו דבר
רק להבין את הרעיון)הבאת דוגמא של Println, ואז אתה שואל על שמירת התוצאה
תן לנו דוגמא איך ב-go אתה שומר את התשובות שמתקבלות מהפונקציות הא-סינכרוניות, יהיה יותר קל להסביר עם "דוגמא דומה". -
@אהרן
אני מקווה שאני לא משגע כאן את הציבור
אבל לי יש הרגלי למידה מוזרים
ואני לומד דברים מהאמצע, ואז חוזר לבסיס.זה לא בגלל שזו דרך חכמה
אלא בגלל שיש לי נכות שאני לא מצליח ללמוד דברים שמשעממים אותי
אז אני לוקח את העניין של "ליבו חפץ" בהידור, ולומד רק מה שליבי חפץ.ללמוד JS לפי הסדר משעמם אותי ברמות אני לא מצליח לשבת על מדריך יותר מכמה דקות
אז הפתרון שלי הוא לקפוץ ישר לדברים המעניינים
ומתוך זה להגיע ליסודות הבסיסיים. -
@אהרן אמר באיך עובד קוד אסינכרוני בJS?:
הבאת דוגמא של Println, ואז אתה שואל על שמירת התוצאה
תן לנו דוגמא איך ב-go אתה שומר את התשובות שמתקבלות מהפונקציות הא-סינכרוניות, יהיה יותר קל להסביר עם "דוגמא דומה".מה הכוונה "שומר"
כשאני אומר שומר אני מתכוון לכל שימוש בערך
זה יכול להיות הדפסה,שמירה במסד נתונים או כל שימוש אחר. -
@nigun אמר באיך עובד קוד אסינכרוני בJS?:
בJS הדרך לנהל את התוצאה שמתקבל מבקשת HTTP (למשל)
היא לעשות פונקציה בתוך פונקציה
וכך אנחנו תופסים את התגובה, בלי שהיא "תברח" לנוהבנת טוב טוב למה אי אפשר לתפוס אותה במשתנה פשוטה?
אם כן אתה על דרך המלך. אבל עוד לא הגעת ליעד...
אתה צודק שפרומיס עוזר לנו לתפוס את התשובה במשתנה.
אבל פספסת עוד נקודה חשובה שחוץ ממה שאחנו רוצים שלא יברח לנו אנחנו גם חייבים לקבל הודעה על סיום הפעולה. ואז נרצה להמשיך את שרשרת הפעולות עם השלב הבא.
כבר הזכרנו למעלה שאי אפשר בשום מחיר להפסיק את ה-thread הראשי שיחכה לאירוע מסויים.
אז חייבים callback-ים.
עכשיו האתגר שעמדה לפני מהנדסי השפה הוא למצוא את הדרך הכי אלגנטית לתכנת עם ספגטי של callback-ים...מה שכתבת למעלה לא מדוייק כי:
הPromise שומר את הערך במשתנה בזיכרון (או אולי מערך)
אתה חשבת על פתרון אחד לבעיה שאותה הבנת, (שאי אפשר לתפוס את הערך של התשובה). הפתרון של פרומיס הוא בצורה שונה. לא במשתנה גלובלי.
ואז אפשר לגשת למשתנה הנ"ל מכל מקום בקוד ולבדוק את הערך.
כנ"ל, אי אפשר לגשת ישירות למשתנה כמו במשתנה גלובלי.
אפשר למסור ל-runtime פונקציה שמורצת עם התשובה כארגומנט.