ECMAScript 6 Promises

פיצ'ר חשוב ומשמעותי מאוד ב-ECMAScript 6 המאפשר אסינכרוניות מלאה בקלות.

על promises כתבתי כבר בעבר. לפי דעתי מדובר בפיצ'ר החשוב ביותר של ES6. בעולם הג'אווהסקריפט של היום, יש לנו המון המון תקשורת עם שרתים אחרים. וזה לא משנה אם אני כותב סקריפט בצד הלקוח או בצד השרת. אם מדובר בצד השרת, node.js הרי מאוד חזקה ב-I\O (שזה פלט קלט) ויש לי המון קריאות למסד הנתונים, לשרתים אחרים או למערכת הקבצים. הקריאות תלויות זו בזו ומה שקורה בדרך כלל הוא ש-callback קורא ל-callback ואז נוצר לנו callback hell. גם בצד הלקוח – לפעמים יש קריאות שתלויות זו בזו. לבק אנד ואז שוב פעם לבק אנד ואז לשרת סטטיסטיקות וגם שם אני מגיע ל-callback hell. מה זה הגיהנום הזה? משהו כזה:


        }
      })
    })
  }
})

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

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


 myPromise.then(function(data) {
//success
  }, function(error) {
//fail
  })

כאן אני מראה אובייקט ששמו הוא myPromise והוא מסוג promise. זו 'ההבטחה' שלי. עוד מעט נדבר על איך אני מייצר אותה. אבל אובייקט ההבטחה הזה הוא ייחודי. הוא יכול להיכשל ויכול להצליח. אני פשוט קובע מה הוא יעשה אם ההבטחה מצליחה (זה ה-callback הראשון שמועבר ב-then) או אם ההבטחה ניכשלת (זה ה-callback השני).

אני יכול גם לשרשר הבטחות. כי כל then מחזיר אוטומטית הבטחה. כלומר זה נראה ככה:


 myPromise.then(function(paramater) {
    console.log('Got data! Promise fulfilled.');
    return data;
  }, function(parameter2) {
    console.log('Promise rejected.')
.then(function(data){
   //do another thing, it will be called after the first promise was fullfilled.
}).
.then(function(data){
   //do third thing, it will be called after the last promise was fullfilled.
});

וככה אני נמלט מה-callback hell. איך אני יוצר את אובייקט ההבטחה המיוחד? באמצעות האובייקט Promise ואני אדגים:


 var myPromise = new Promise(function(resolve, reject) {
        resolve(paramater); // Promise resolved! 
        reject(parameter2); // Promise rejected! 
    };
  });

אני יודע שזה לא מוחשי, אז בואו נהפוך את זה למוחשי:


var myPromise = new Promise(function(resolve, reject) {
        resolve('promise resolved'); 
});

myPromise.then(function(data) {
    return data + ' 1 ';
})
.then(function(data){
   return data + ' 2 ';
})
.then(function(data){
   console.log(data); //promise resolved 1  2 
});

אם תריצו את הדבר הזה, תראו שבקונסולה תקבלו "promise resolved 1 2". בואו ונראה למה.

בחלק הראשון, אני יוצר אובייקט מסוג promise ומכניס אותו לתוך myPromise. הקוד שבתוך ה-callback רץ מייד. במקרה הזה אני מבצע resolve מיידי. כלומר ממלא את ההבטחה מייד עם הטקסט promise resolved.
למי בדיוק אכפת מזה שההבטחה מולאה? לקוד שבא מייד אחריו – myPromise.then. בתוכו אני מכניס then שמתבצע רק אם ההבטחה מתמלאת. מה הוא עושה? מוסיף 1 למידע שמגיע (במקרה הזה הטקסט promise resolved) ומחזיר אותו. למי הוא מחזיר אותו? ל-then שמשורשר מיד אחריו והוא לוקח את המידע, מוסיף 2 ומשרשר אותו הלאה ל-then השלישי שמדפיס את כל הסיפור הזה.

מסובך? בואו ונראה דוגמה יותר ממשית ויותר פשוטה, בלי שרשורים. נניח ואני רוצה להדפיס בדיחה מתוך API. אני רוצה להשתמש ב-API שמספר בדיחות (נשבע לכם) שכתובתו היא: https://api.icndb.com/jokes/random. אם תיכנסו לכתובת הזו תראו שאתם מקבלים בדיחה רנדומלית. אני רוצה להציג אותה.

כך אני אממש את הקוד עם promise:


var myPromise = new Promise(function(resolve, reject) {
  var request = new XMLHttpRequest();

  request.open('GET', 'https://api.icndb.com/jokes/random');
  request.onload = function() {
    if (request.status == 200) {
      resolve(request.response); // we got data here, so resolve the Promise
    } else {
      reject(Error(request.statusText)); // status is not 200 OK, so reject
    }
  };

  request.onerror = function() {
    reject(Error('Error fetching data.')); // error occurred, reject the  Promise
  };

  request.send(); //send the request
});

myPromise.then(function(data) {
  console.log('Got data! Promise fulfilled.');
  document.getElementsByTagName('body')[0].textContent = JSON.parse(data).value.joke;
}, function(error) {
  console.log('Promise rejected.');
  console.log(error.message);
})

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

מה הולך פה? קודם כל אני בונה את ה-promise שלי. בתוכו אני יוצר קריאת AJAX שיכולה להצליח או להיכשל. אם היא מצליחה, היא מקיימת את ההבטחה ומעבירה את הנתונים. אם היא נכשלת אני מכשיל את ההבטחה.

ב-myPromise אני קולט את ההבטחה. הקוד הזה לא ירוץ כל עוד קריאת ה-AJAX לא הצליחה/נכשלה. ברגע שהקריאה הצליחה, אני מדפיס את הבדיחה. אם היא תיכשל, אני אכתוב הודעה ל-log.

אם אתם עדיין לא מבינים, שחקו עם הקוד. זה חשוב.

Promise ALL

אם יש לי כמה promises, אני יכול לשרשר אותן ולהריץ פונקציה שתרוץ רק אם כולן יצליחו. לדוגמה:


var myPromise1 = new Promise(function(resolve, reject) {
  var request = new XMLHttpRequest();

  request.open('GET', 'http://api.icndb.com/jokes/random');
  request.onload = function() {
    if (request.status == 200) {
      resolve(request.response); // we got data here, so resolve the Promise
    } else {
      reject(Error(request.statusText)); // status is not 200 OK, so reject
    }
  };

  request.onerror = function() {
    reject(Error('Error fetching data.')); // error occurred, reject the  Promise
  };

  request.send(); //send the request
});

var myPromise2 = new Promise(function(resolve, reject) {
  var request = new XMLHttpRequest();

  request.open('GET', 'http://numbersapi.com/42/trivia');
  request.onload = function() {
    if (request.status == 200) {
      resolve(request.response); // we got data here, so resolve the Promise
    } else {
      reject(Error(request.statusText)); // status is not 200 OK, so reject
    }
  };

  request.onerror = function() {
    reject(Error('Error fetching data.')); // error occurred, reject the  Promise
  };

  request.send(); //send the request
});

Promise.all([myPromise1, myPromise2])
  .then(function(data) {
  document.getElementsByTagName('body')[0].textContent=JSON.parse(data[0]).value.joke;
  document.getElementsByTagName('body')[0].textContent+=data[1];
  console.log(data)
}, function (err) {
  console.log(`error: ${err}`)
})

See the Pen Native ALL promise example by Ran Bar-Zik (@barzik) on CodePen.

לא להבהל 🙂 יש לי כאן promise1 ו-promise2. שתיהן promise רגילים שכבר הראיתי איך ליצור. אחת פונה ל-API אחד והשניה פונה לאחר. על מנת להריץ קוד שירוץ רק לאחר ששתי ההבטחות יתמלאו, אני אשתמש במתודת Promise.all שמקבלת מערך של כל ההבטחות שאמורות להתמלא. במידה וכן, אני מקבל מערך (data במקרה הזה) שמורכב מהמידע שמעבירות לי כל ההבטחות ואני יכול לעשות בו כרצוני.

יש לנו את מתודת race שעובדת על אותו עקרון של all, אבל היא רצה ברגע שקריאה אחת מצליחה לעבור וזונחת את כל השאר. אני לא השתמשתי בה מעולם וקשה לי לחשוב על scenario שבו אשכרה משתמשים בה.

בגדול – זה אחד הפיצ'רים החשובים של ES6 וחשוב מאוד להכיר אותו.

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

צילום מסך של סוואגר
יסודות בתכנות

openAPI

שימוש בתשתית הפופולרית למיפוי ותיעוד של API וגם הסבר בסיסי על מה זה API

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