JS Promise

‏ • 2 באוקטובר, 2020

נושא ה promise אינו מיוחד דווקא לאנגולר, אך השימוש בו נפוץ ומומלץ לפי הצורך.

להלן דוגמא הממחישה את הבעיה בשבילה נוצר הצורך ב – promise.

פונקציות a,b הינן פונקציות פשוטות והגדרתן היא:

function a(){
  console.log("a is done");
}
function b(){
  console.log("b is done");
}
a();
b();

הפלט הוא, לפי הסדר, קודם פונקציה a מתבצעת ואח"כ פונקציה b מתבצעת:

a is done
b is done

הרחבת הפונקציות:

הוספת משתנה גלובלי בשם sum.

פונקציית a תכפיל ב 10, אחרי הכפלה זו פונקציה b תוסיף 3.

var sum = 5;
function a(){
sum=sum*10;
console.log("a is done, sum is " + sum);
}
function b(){
sum=sum+3
console.log("b is done, sum is " + sum);
}
a();
b();

הפלט יהיה:

a is done, sum is 50
b is done, sum is 53

עד כאן הכל טוב.

מה קורה אם לפונקצית a לוקח קצת יותר זמן לפעול, לא סיימה את המשימה, וכבר b מתחילה להתבצע? נניח בקריאה לנתונים מתוך סרבר, או נניח בשימוש בפונקצית setTimeout? הערך של sum לא יהיה תקין. יש צורך לוודא שפונקציהb  תופעל אך ורק אם פונקציה a סיימה את פעולתה.

פונקציהsetTimeout  היא פונקציה שמקבלת כפרמטר 2 נתונים: פונקציה להפעלה, ומספר מילישניות. היא מפעילה את הפונקציה לאחר מספר המילישניות שניתנו.

לפי הדוגמא, פונקציה a תפעיל את התוכן שלה לאחר 2 שניות, ואילו פונקציה b תפעיל

את התוכן שלה לאחר שניה אחת:

var sum = 5;
function a(){
setTimeout(() => {
sum=sum*10;
console.log("a is done, sum is " + sum);
}, 2000);
}

function b(){
setTimeout(() => {
sum=sum+3
console.log("b is done, sum is " + sum);
}, 1000);
}
a();
b();

הפלט יהיה:

b is done, sum is 8
a is done, sum is 80

התוצאה שגויה. פונקציה b אינה מופעלת לאחר סיום פעולת פונקציה a.

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

הפתרון שיצרו עד עכשו היה:

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

כך:

var sum = 5;
function a(myfunc){
setTimeout(() => {
sum=sum*10;
console.log("a is done, sum is " + sum);
myfunc();
}, 2000);
}

function b(){
setTimeout(() => {
sum=sum+3
console.log("b is done, sum is " + sum);
}, 1000);
}
a(b);

הפלט יהיה:

a is done, sum is 50
b is done, sum is 53

כלומר, פונקציהb  הופעלה רק אחרי סיום ביצוע פונקציה a.

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

הפתרון הוא Promise.

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

ההגדרה:

var promise = new Promise((resolve, reject) => { });

בתוך הסוגריים המסולסלים ייכתב תוכן הפונקציה. בקריאה ל promise() תוכן הסוגריים המסולסלים יופעל.

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

reject – מעין "דגל" שמורם, ערך שמודלק ומודיע שקיימת שגיאה. לא בזמן שבאמת יש שגיאת error, שאז התוכנה לבד כבר מזהה את השגיאה ואותה יש לתפוס בעזרת try-catch. אלא, נניח היה ניסיון לשלוף נתונים, והנתונים הגיעו ריקים, יהיה חסר טעם להמשיך לקרוא לפונקציות הבאות, ולכן יש להודיע על "שגיאה". כלומר, קיימת תקלה. אין להמשיך לקרוא ליתר הפונקציות המקושרות בזו אחר זו.

לדוגמא:

var promise = new Promise((resolve, reject) => { 
setTimeout(() => {
console.log("promise is done");
resolve(); }, 1000); });

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

קיימת בעיה קטנה: ברגע שמוגדר promise new, נוצרת הפעלה של הפונקציה ולא רק הגדרה.

כמו כן, יתכן ויש צורך שה-promise יהיה רב שימושי, ולכן, מקובל לעטוף זאת בפונקציה אנונימית, שתחזיר promise:

function a() { 
var promise = new Promise((resolve, reject) => { 
setTimeout(() => { 
console.log("a is done"); 
resolve();
}, 1000); }); 
return promise;
}
a();

הפלט יהיה:

a is done

נניח, יש צורך לעצור הפעלת פונקציות שמקושרות ככאלה שצריכות להתבצע אחת אחרי השנייה. אך כאשר sum הוא אפס יש להתריע על כך באמצעות פונקציה resolve:

var sum = 0;
function a() { 
var promise = new Promise((resolve, reject) => { 
setTimeout(() => {
if (sum==0){
reject();}
else{
console.log("a is done"); 
resolve();} }, 1000); }); 
return promise; }
a();

הפלט יהיה:

Uncaught (in promise)

במידה ומפועל ה reject, הפונקציה נעצרת באותו רגע ולא ממשיכה הלאה, ולא רק הפונקציות הבאות שמקושרות (בהמשך החומר).

שינוי ערך sum:

var sum = 5;
function a() { 
var promise = new Promise((resolve, reject) => { 
setTimeout(() => {
if (sum==0)
reject();
console.log("a is done"); 
resolve(); }, 1000); }); 
return promise; }
a();

לאחר שינוי ערך ל sum=5, הדגל resolve יורם, ותהיה אפשרות לקרוא לפונקציה הבאה להתבצע.

לדוגמא:

a().then(() => console.log("ready for the next!"));

הפלט יהיה:

a is done
ready for the next!

פונקציה a() תתבצע, ברגע שיורם הדגל resolve() שיכול להיות גם באמצע הפונקציה, פונקציה then תופעל, ותבצע את הקוד שיש בתוכה.

אם הורם הדגל של reject, ה-then לא יתבצע.

נניח הוגדר promise, ולא היתה קריאה ל resolve כלל:

function a() { 
var promise = new Promise((resolve, reject) => { 
setTimeout(() => {
console.log("a is done"); 
}
, 1000);
}); 
return promise; 
}

ה then שיקושר לאחר קריאה ל a לעולם לא יתבצע. הוא מוכרח לקבל איתות מהדגל resolve לזמן בו הוא יופעל.

הקריאה לפונקציה a:

a().then(() => console.log("ready for the next!"));

הפלט יהיה:

a is done

להפעלת קוד במידה ומתבצע reject, יש להגדיר את הקוד כפרמטר שני לפונקצית then כך:

function a() { 
var promise = new Promise((resolve, reject) => { 
setTimeout(() => {
console.log("a is now working"); 
reject();
}
, 1000);
}); 
return promise; }
a().then( () => console.log("ready for the next!"), 
() => console.log("a with an err"))

הפלט יהיה:

a is now working
a with an err

כלומר, ברגע שהופעל ה-reject של פונקציה a, הופעל הקוד שנמצא כפרמטר שני של פונקציית then.

להפעלת פקודה רק במקרה של reject, ניתן לכתוב כך:

a().then(null,
          () => console.log("a with an arr"))

קבלת פרמטרים מתוך הפונקציה אל תוך ה-then

לצורך העברת נתונים מתוך ה promise החוצה אל ה then(), יש לשלוח את הנתון בפונקציה של resolve כך:

resolve("perfect")

הנתון יישתל בתוך הסוגריים של ה then()  כך:

a().then((val)=>console.log(val);

לתוך val נכנס הערך הרשום בתוךresolve  ובמקרה זה, התוכן יהיה "perfect".

כנ"ל לגבי reject.

לדוגמא מלאה:

var sum = 1;
function a() { 
var promise = new Promise((resolve, reject) => { 
setTimeout(() => {
console.log("a is now working");

if(sum==0)
reject("do not continue, the sum is " + sum);
resolve("the sum is " + sum);
}
, 1000);
}); 
return promise; }

a().then((val1) => console.log(val1), 
(val2) => console.log(val2))

הפלט יהיה:

a is now working
the sum is 1

דוגמאות נרחבות – הפעלת 3 פונקציות

להלן הגדרה של 3 פונקציות:

var firstMethod = function() {
var promise = new Promise(function(resolve, reject){
setTimeout(function() {
console.log('first method completed');
resolve({data: '123'});
}, 2000);
});
return promise;
};
var secondMethod = function(val) {
var promise = new Promise(function(resolve, reject){
setTimeout(function() {
console.log('second method completed');
console.log(val.data);
resolve(val);
}, 2000);
});
return promise;
};

var thirdMethod = function(someStuff) {
var promise = new Promise(function(resolve, reject){
setTimeout(function() {
console.log('third method completed');
resolve("3 is done")
}, 1000);
});
return promise;
};

האופן בו אפשר לקרוא לפונקציה אחת:

firstMethod();

הפלט יהיה:

first method completed

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

קריאה לפונקציה שניה מיד אחרי שהראשונה מתבצעת:

firstMethod().then((val)=>secondMethod(val));

הפלט יהיה:

first method completed
second method completed
123

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

firstMethod().then(secondMethod);

הפלט יהיה:

first method completed
second method completed
123

קריאה לשלושתם ברצף:

firstMethod()
.then(secondMethod)
.then(thirdMethod);

הפלט יהיה:

first method completed
second method completed
123
third method completed

ניתן לשנות את השם של הפונקציות resove, reject אך לא מקובל:

var firstMethod = function() {
var promise = new Promise(function(resolve1, reject){
setTimeout(function() {
console.log('first method');
resolve1('first method completed');
}, 2000);
});
return promise;
};

שימוש בערך שהתקבל מהפונקציה הקודמת, וזריקת שגיאת מערכת יזומה:

var secondMethod = function(val) {
var promise = new Promise(function(resolve, reject){
setTimeout(function() {
console.log(val);
throw new Error("fail!!!")
resolve('second method completed');
}, 2000);
});
return promise;
};

var thirdMethod = function(someStuff) {
var promise = new Promise(function(resolve, reject){
setTimeout(function() {
console.log('third method completed');
resolve({result: someStuff.newData});
}, 1000);
});
return promise;
};

ברגע שקיימת שגיאה, כל שרשרת הקריאה לפונקציות תיעצר.

 

תגיות: , , ,

תגובות בפייסבוק