MDN wants to learn about developers like you: https://www.surveygizmo.com/s3/5171903/MDN-Learn-Section-Survey-Recruiter-Pathway

This translation is incomplete. Please help translate this article from English.

במאמר זה אנחנו נסקור שוב את הבעיות הקשורות ב-Synchronous JavaScript, ונסתכל לראשונה על הטכניקות לא-סינכרוניות שניתקל בהן, וכיצד הן יכולות לסייע לנו לפתור בעיות אלו.

ידע מוקדם: Basic computer literacy, a reasonable understanding of JavaScript fundamentals.
מטרה: להבין מהי שAsynchronous JavaScript וכיצד היא שונה מ-Synchronous JavaScript, ומתי להשתמש בה.

Synchronous JavaScript

על מנת שנוכל להבין מה זה asynchronous JavaScript אנחנו צריכים תחילה להבין מה זה synchronous JavaScript. חלק זה של המאמר יסקור שוב חלק מהמידע שכבר עברנו עליו במאמר הקודם.

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

const btn = document.querySelector('button');
btn.addEventListener('click', () => {
  alert('You clicked me!');

  let pElem = document.createElement('p');
  pElem.textContent = 'This is a newly-added paragraph.';
  document.body.appendChild(pElem);
});

בקוד זה, השורות מורצות אחת אחרי השנייה:

  1. אנחנו יוצרים הפנייה לאלמנט <button> שכבר זמין ב-DOM.
  2. אנחנו מוסיפים מטפל אירוע בשם click כך שכאשר הכפתור נלחץ:
    1. הודעת ()alert מוצגת למשתמש.
    2. ברגע שהודעה זו נסגרת, אנחנו יוצרים אלמנט <p>.
    3. לאחר מכן אנחנו מכניסים לתוך האלמנט  <p> תוכן.
    4. בסוף אנחנו משייכים את ה- <p> ל-body.

בעוד שכל תהליך שכזה נמצא בהרצה או עיבוד, שום דבר אחר לא יכול להתרחש - יתר העיבוד מושהה. זה מכיוון שכפי שראינו במאמר הקודם ש-JavaScript היא single threaded. רק דבר אחד יכול להתרחש בכל פעם על ה-single main thread, וכל היתר חסום לביצוע או לעיבוד עד אשר אותה פעולה תושלם.

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

לתשומת לב: חשוב לזכור שאמנם ()alert מאוד שימושי לשם הסבר על פעולות סינכרוניות שחוסמות את המשך הפעילות, זוהי פונקציה שאינה בשימוש רב ביישומים אמיתיים מטעמים של חווית משתמש לרוב.

Asynchronous JavaScript

לאור הסיבות שנסקרו למעלה, הרבה Web API משתמשים בקוד א-סינכרוני על מנת לפעול, במיוחד אלו שניגשים או מביאים משאב מסויים מגורם חיצוני, כמו הבאה של קובץ מהרשת, גישה למאגר מידע והחזרת מידע מתוכו, הפעלת של הזרמת וידאו באמצעות מצלמת רשת וכד׳.

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

var response = fetch('myImage.png');
var blob = response.blob();
// display your image blob in the UI somehow

זה מכיוון שאנחנו לא יודעים כמה זמן ייקח לתמונה להגיע מהשרת, אז כאשר אנחנו נרצה להריץ את שורת הקוד הבאה, היא תחזיר לנו שגיאה (אולי לסירוגין, אולי בכל פעם) מכיוון שה-response אינו זמין עבורנו עדיין. במקום זאת, אנחנו צריכים שהקוד שלנו יחכה ל-response שיוחזר אלינו לפני שאנחנו מנסים לעשות עם response פעולה כלשהי.

יש שני סוגים עיקריים של קוד א-סינכרוני שאנחנו ניתקל בהם בקוד javascript, יש את ה-callbacks הותיקים ואת ה-promises החדשים יותר.  במאמר זה נסקור את שניהם.

Async callbacks

Async callbacks אלו פונקציות המועברות כפרמטר (ארגומנט) לפונקציה אחרת, כאשר אנחנו קוראים לפונקציה אחרת אשר מריצה קוד ברקע. כאשר הקוד שברקע סיים לרוץ, הוא קורא לאותן פונקציות callbacks על מנת לציין שהפעולה שהייתה ברקע הסתיימה או לציין שמשהו הסתיים.

שימוש ב-callbacks יחסית נחשב מיושן כעת, אבל אנחנו עדיין נראה שימוש רב שלהם ב-APIs ישנים אבל עדיין מאוד שימושיים.

דוגמא ל-async callback אפשר לראות בפרמטר השני של addEventListener() (כפי שראינו בפעולה למעלה):

btn.addEventListener('click', () => {
  alert('You clicked me!');

  let pElem = document.createElement('p');
  pElem.textContent = 'This is a newly-added paragraph.';
  document.body.appendChild(pElem);
});

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

כאשר אנחנו מעבירים callback function כפרמטר לפונקציה אחרת, אנחנו רק מעבירים את הגדרת הפונקציה כפרמטר, כלומר ה-callback function לא מופעל באופן מיידי. הוא נקרא לאחר מכן (“called back”) באופן א-סינכרוני איפשהו בתוך הגוף של הפונקציה שקיבלה אותו כפרמטר. הפונקציה שקיבלה אותו כפרמטר היא האחראית להפעיל את ה-callback function כשנצטרך.

אנחנו יכולים לכתוב פונקציות משלנו שיכילו callback function באופן דיי פשוט יחסית. נסתכל על דוגמא שמעלה משאב באמצעות XMLHttpRequest API (דף האינטרנט, ו- קוד המקור):

function loadAsset(url, type, callback) {
  let xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.responseType = type;

  xhr.onload = function() {
    callback(xhr.response);
  };

  xhr.send();
}

function displayImage(blob) {
  let objectURL = URL.createObjectURL(blob);

  let image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
}

loadAsset('coffee.jpg', 'blob', displayImage);

כאן יצרנו את פונקציית ()displayImage שפשוט מקבלת blob שמועבר אליה ויוצרת לו URL באמצעות URL.createObjectURL(blob). לאחר מכן הפונקציה יוצרת אלמנט HTML מסוג img, משימה לו את הערך של ה-src לאותו URL שנוצר לנו ומשייכת את ה-image ל-body. 

בנוסף, יצרנו פונקציה בשם ()displayImage שמקבלת כפרמטרים כתובת URL, סוג הקובץ וכן פונקציית callback (שימו לב שהשם שנתנו לפרמטר - callback - הוא לשם הנוחות בלבד וניתן לקרוא לפרמטר זה בכל שם). פונקציית ()displayImage משתמשת ב-XMLHttpRequest (לרוב משתמשים בקיצור שלו - "XHR") על מנת להביא משאב מ-URL מסויים לפני שמעבירים את התגובה של אותה XMLHttpRequest לפונקציית callback שלנו - לפונקציית  ()displayImage.

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

Callbacks הם מאוד ורסטיליים - לא רק שהם מאפשרים לנו לשלוט בסדר שבו פונקציות ירוצו ואיזה מידע יועבר ביניהן, הן גם מאפשרות לנו להעביר מידע לפונקציות שונות בהתאם לנסיבות. כך שאנחנו יכולים להריץ פעולות שונות על המשאב שהתקבל או התגובה שהתקבלה כמו processJSON(), displayText(), וכד׳. 

שימו לב שלא כל ה-Callbacks הם א-סינכרוניים וחלקם הם סינכרוניים. כך לדוגמא, כאשר אנחנו משתמשים ב- Array.prototype.forEach() על מנת לעבור באמצעות לולאה על איברים במערך (ראו כדף אינטרנט, וכן את קוד המקור):

const gods = ['Apollo', 'Artemis', 'Ares', 'Zeus'];

gods.forEach(function (eachName, index){
  console.log(index + '. ' + eachName);
});

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

Promises

Promises אלו בעצם הסגנון החדש לקוד א-סינכרוני שאנחנו נראה שמבוצע בהם שימוש ב-Web APIs מודרניים. דוגמא טובה לכך היא fetch() API, אשר הוא בעצם כמו גרסה מודרנית ויעילה יותר של XMLHttpRequest. נסתכל על דוגמא מהמאמר שלנו בנושא הבאת מידע מהשרת אשר תגיעו אליו בהמשך:

fetch('products.json').then(function(response) {
  return response.json();
}).then(function(json) {
  products = json;
  initialize();
}).catch(function(err) {
  console.log('Fetch problem: ' + err.message);
});

לתשומת לב: אתם יכולים למצוא את הגרסה הסופית ב- GitHub (ראו כאן את קוד המקור, וגם כדף אינטרנט).

כאן אנחנו רואים ש-fetch() לוקח פרמטר אחד - את ה-URL של המשאב שנאחנו רוצים להביא מהרשת - והוא מחזיר promise. ה-promise היא בעצם אובייקט המייצג השלמה או כישלון של פעולת ה-שaysync. בצורה מסויימת, זו הדרך של הדפדפן להגיד ״אני מבטיח לחזור אליך עם תשובה ברגע שאוכל״. מכאן השם promise.

צריך להתרגל לרעיון הזה על ידי תרגול. זה מרגיש קצת מוזר בהתחלה, כמו החתול של שרדינגר - Schrödinger's cat. אף אחת מהאפשרויות לא קרתה עדיין, אז פעולת ההבאה - פעולת ה-fetch, כרגע מחכה לתוצאה של פעולת הדפדפן - להשלמה שלה בעתיד. לאחר מכן שיש לנו שלושה קודי בלוק שמשורשרים לסוף fetch():

  • שני בלוקים של then(). שניהם מכילים callback function שתרוץ אם הפעולה שקדמה לה הצליחה, וכל callback מקבל כקלט, את התוצאה של הפעולה המוצלחת הקודמת, כך שאנחנו יכולים להמשיך ולעשות עם התוצאה משהו. כל .then() מחזיר הבטחה - promise נוספת, כלומר אנחנו יכולים לקשור כמה וכמה בלוקי קוד של .then() אחד לשני, כך שיהיוה הרבה פעולות א-סינכרוניות שירוצו בסדר מסויים, אחת אחרי השנייה.
  • בלוק הקוד של catch() בסוף ירוץ אם אחד מהבלוקים של .then() ייכשל - בדרך דומה ל-try...catch הסינכרוני. אובייקט שגיאה - error object - ייהפך לזמין בתוך ה-catch(), והוא יוכל לשמש עבור דיווח על סוג השגיאה שהתרחשה. שימו לב כי promises הסינכרוני לא עובד עם try...catch, למרות שהוא כן יעבוד עם async/await, כפי שנראה בהמשך המאמר.

לתשומת לב: אתם תלמדו עוד הרבה על 9999 בהמשך המודול הזה, אז אל דאגה אם לא הבנתם אותם עד הסוף. זוהי רק סקירה.

The event queue

פעולות א-סינכרוניות כמו promises מושמות לתוך ה-event queue, אשר רץ לאחר שה-main thread סיים את העיבודים שלו, כך שהם לא יחסמו קוד JavaScript שבא אחריהם. הפעולות שב-9099999 יושלמו ברגע שיתאפשר, ויחזירו את התוצאה שלהן לסביבת הjavascript. Async operations like promises are put into an event queue, which runs after the main thread has finished processing so that they do not block subsequent JavaScript code from running. The queued operations will complete as soon as possible then return their results to the JavaScript environment.

Promises מול callbacks

ל-Promises יש קצת דמיון ל-callbacks הוותיקים. הם בעיקרון אבוייקט שהוחזר, אליו אנחנו מחברים פונקציות callbacks, ולא צריכים להעביר callbacks לתוך פונקציה.

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

  • ראשית, אנחנו יכולים לקשור מספר פעולות א-סינכרוניות ביחד באמצעות שימוש בכמה .then(), והעברה התוצאה של אחד כקלט של זה שאחריו. זה הרבה יותר קשה לביצוע עם callbacks, מה שבדרך מסתיים עם "pyramid of doom", אשר ידוע גם כ-ה
  • Promise callbacks תמיד ייקראו בסדר שבו הם מושמים ב- event queue.
  • Error טיפול ב- הוא הרבה יותר קל - כל השגיאות מטופלות על ידי בלוק .catch() אחד בסוף הבדלוק, ולא באמצעות טיפול בכל שלב של ה-פירמידה.
  • Avoid inversion control: unlike callbacks will lose full control of how the function will be executed when passing a callback to a third-party library. A great demonstration by Stevie Jay.

ההתנהגות של קוד א-סינכרוני

בואו נעמיק בדוגמא שתסביר לנו יותר לעומק את ההתהנגות של קוד א-סינכרוני, שמראה מה יכול לקראת כשאנחנו לא בקיאים לגמי בסדר של הרצת הקוד וההבעיות שיש בניסיון לטפל בקוד א-סינכרוני כמו בקוד סינכרוני. הודמא הבא היא יחסית דומה למה שראינו בעבר. sכדף אינטרנט, וגם קוד המקור). הבדל אחד הוא שכללנו מספר ביטויים של console.log() על ממנת להמחיש את הסדר שאנחנו נחשוב שבו הקוד ירוץ

console.log ('Starting');
let image;

fetch('coffee.jpg').then((response) => {
  console.log('It worked :)')
  return response.blob();
}).then((myBlob) => {
  let objectURL = URL.createObjectURL(myBlob);
  image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
}).catch((error) => {
  console.log('There has been a problem with your fetch operation: ' + error.message);
});

console.log ('All done!');

הדפדפן יתחיל להריץ את הקוד, הוא יראה את ה-console.log() הראשון ויריץ אותו. ולאחר מכן ייצור את המשתנה image .

הוא לאחר מכן ימשיך לשורה הבאה, ויתחיל להריץ את הבלוק קוד של fetch(), אבל מכיוון ש-fetch() מורץ באופן א-סינכרוני בלי חסימה, הדפדפן ימשיך לקוד שלאחר הקוד של ה-promise-related code, ולכן יגיע ל-console.log() האחרון ( All done!) ואותו יציג לקונסולה. ימשיך לרוץ

רק ברגע שהבלוק של ה-code>fetch() סיים לחלוטין לרוץ והביא תוצאה - result באמצעות הבלוקים של .then(), אנחנו נראה לבסוף את הההודעה שב-console.log() השני (It worked ;)). כך שההודעות הופיעו בסדר שונה ממה שאולי חשבתם:

  • Starting
  • All done!
  • It worked :)

אם זה מבלבל אתכם, הסתכלו על הדוגמא הבאה:

console.log("registering click handler");

button.addEventListener('click', () => {
  console.log("get click");
});

console.log("all done");

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

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

על מנת לראות זאת בפעולה, נסו לעשות עותק מקומי של To see this in action, try taking a local copy of הדוגמאו תשלנו, ושנו את ה- console.log() שלישית כך:

console.log ('All done! ' + image + 'displayed.');

אתם אמורים לקבל שגיאה בקונסולה במקום ההודעה השלישית :

TypeError: image is undefined; can't access its "src" property

זה מכיוון שבזמן שהדפדפן מנסה להריץ את ה-console.log() השלישית, הבלוק קוד של fetch() עדיין לא סיים לרות, כך שהמשתנה image לא ניתן לו ערך עדיין.

למידה עצמאית: פכו את הכל ל- async!

על מנת לתקן את ה- fetch(), ולהפוך את שלושת -console.log() להיות מופעים בסדר הרצוי, אתם יכולים לעשות את ה-console.log() השלישי שירוץ גם הוא בצורה א-סינכרוני. את זה ניתן לעשות באמצעות העברה שלו לתוך .then() אחר, אשר משורשר לתוך הסוף של ה-.then() השני, או באמצעות פשוט העברה שלו לתוך ה-.then() השני. נסו לתקן זאת.

לתשומת לב: אם נתקעתם, אתם יכולים למצוא את התשובה כאן או כ דף אינטרנט also). אתם גם יכולים למצוא עוד הרבה על promises במדריך שלנו בנושא טיפול בפעולות א-סינכרוניות באמצעות Promises, בהמשך המודול הזה.

לסיכום

במבנה הבסיסי שלה, JavaScript היא סינכרונית, שפה single-threaded,כך רק פעולה אחת יכולה להיות מעובדת בכל זמן נתון. יחד עם זאת, הדפדפנים הגדירו פונקציות ו-APIs שמאפשרים לנו לרשום פונקציות שלא ירוצו באופן סינכרוני, ובמקום זאת, יופעלו באופן א-סינכרוני כאשר אירוע מסויים מתרחש. זה אומר שאנחנו יכולים לתת לקוד שלנו לעשות דברים אחרים באותו הזמן, מבלי לחסום או לעצור את ה-main thread.

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

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

אם אנחנו מריצים פעולה מסויימת שלוקח לה זמן, כמו לדוגמא גישה למאגר מידע ושימוש בתוצאות על מנת ליצור תבניות לדגומא, זה יהיה עדיף להעביר את הפעולה הזו מחוץ ל-main thread, כך שתתצבע באופן א-סינכרוני. עם הזמן, אתם תלמדו מתי זה היה הגיוני לבחור בטכניקה א-סינכרונית ולא בסינכורנית.

במודול זה

Document Tags and Contributors

Contributors to this page: ItzikDabush
Last updated by: ItzikDabush,