הבנה של ההבטחות בJavaScript

הקדמה

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

הבנת ההבטחות

הבטחה בקצרה:

"תאר לעצמך שאתה ילד. אמא שלך מבטיחה לך שהיא תקנה לך טלפון חדש בשבוע הבא."

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

זו הבטחה. להבטחה יש שלושה מצבים. הם הם:

  1. ממתין: אתה לא יודע אם תקבל את הטלפון הזה
  2. מלא: אמא שמחה, היא קונה לך טלפון חדש מזה
  3. נדחה: אמא לא שמחה, היא לא קונה לך טלפון

יצירת הבטחה

בואו נמיר את זה לג'אווהסקריפט.

// ES5: חלק 1

var isMomHappy = false;

// Promise
var willIGetNewPhone = new Promise(
    function (resolve, reject) {
        if (isMomHappy) {
            var phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone); // מלא
        } else {
            var reason = new Error('mom is not happy');
            reject(reason); // לדחות
        }

    }
);

הקוד עצמו מדבר בפני עצמו.

להלן כיצד נראה תחביר ה-Promise בדרך כלל:

// תחביר ה-Promise נראה כך
new Promise(function (resolve, reject) { ... } );

צריכת Promises

עכשיו שיש לנו את ה-Promise, בואו נצרוך אותו:

// ES5: חלק 2

var willIGetNewPhone = ... // המשך מחלק 1

// קריאה ל-Promise שלנו
var askMom = function () {
    willIGetNewPhone
        .then(function (fulfilled) {
            // יאי, קיבלת טלפון חדש
            console.log(fulfilled);
             // פלט: { brand: 'Samsung', color: 'black' }
        })
        .catch(function (error) {
            // אופס, האמא לא קנתה אותו
            console.log(error.message);
             // פלט: 'האמא לא שמחה'
        });
};

askMom();

בואו נפעיל את הדוגמה ונראה את התוצאה!

דמו: https://jsbin.com/nifocu/1/edit?js,console

שרשור של הבטחות

הבטחות ניתנות לשרשור.

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

זו הבטחה נוספת. בואו נכתוב אותה!

// ES5

// הבטחה השנייה
var showOff = function (phone) {
    return new Promise(
        function (resolve, reject) {
            var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

            resolve(message);
        }
    );
};

הערות: אנו יכולים לקצר את הקוד הנ"ל על ידי כתיבה כפי שמוצג להלן:

// לקצר אותו

// הבטחה השנייה
var showOff = function (phone) {
    var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

    return Promise.resolve(message);
};

בואו נשרשר את ההבטחות. אתה, הילד, יכול להתחיל את ההבטחה showOff רק לאחר ההבטחה willIGetNewPhone.

// קריאה להבטחה שלנו
var askMom = function () {
    willIGetNewPhone
    .then(showOff) // שרשור כאן
    .then(function (fulfilled) {
            console.log(fulfilled);
         // פלט: 'היי חבר, יש לי טלפון חדש של סמסונג שחור.'
        })
        .catch(function (error) {
            // אופס, אמא לא קנתה אותו
            console.log(error.message);
         // פלט: 'אמא לא שמחה'
        });
};

ככה אתה יכול לשרשר את ההבטחה.

הבטחות הן אסינכרוניות

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

// קריאה להבטחה שלנו
var askMom = function () {
    console.log('before asking Mom'); // רישום לפני
    willIGetNewPhone
        .then(showOff)
        .then(function (fulfilled) {
            console.log(fulfilled);
        })
        .catch(function (error) {
            console.log(error.message);
        });
    console.log('after asking mom'); // רישום אחרי
}

מהי סדרת הפלט הצפוי? ייתכן שתצפו:

1. before asking Mom
2. Hey friend, I have a new black Samsung phone.
3. after asking mom

עם זאת, סדרת הפלט האמיתית היא:

1. before asking Mom
2. after asking mom
3. Hey friend, I have a new black Samsung phone.

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

הנה הדוגמה המלאה ב-ES5:

// ES5: דוגמה מלאה

var isMomHappy = true;

// Promise
var willIGetNewPhone = new Promise(
    function (resolve, reject) {
        if (isMomHappy) {
            var phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone); // מלא
        } else {
            var reason = new Error('mom is not happy');
            reject(reason); // דחייה
        }

    }
);

// Promise שני
var showOff = function (phone) {
    var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

    return Promise.resolve(message);
};

// קריאה ל-Promise שלנו
var askMom = function () {
    willIGetNewPhone
    .then(showOff) // שרשור כאן
    .then(function (fulfilled) {
            console.log(fulfilled);
            // פלט: 'היי חבר, יש לי טלפון סמסונג שחור חדש.'
        })
        .catch(function (error) {
            // אופס, המון לא קנתה אותו
            console.log(error.message);
            // פלט: 'המון לא שמחה'
        });
};

askMom();

Promises ב-ES5, ES6/2015, ES7/Next

ES5 – דפדפנים רבים

קוד הדמו פועל בסביבות ES5 (כל הדפדפנים המרכזיים + NodeJs) אם תכלול את ספריית ה-Promise Bluebird. זה בגלל ש-ES5 לא תומך ב-Promises משלו. ספריית Promise מפורסמת אחרת היא Q על ידי Kris Kowal.

ES6 / ES2015 – דפדפנים מודרניים, NodeJs v6

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

הנה הדוגמה המלאה בקוד ES6:

//_ ES6: דוגמה מלאה_

const isMomHappy = true;

// בטחון
const willIGetNewPhone = new Promise(
    (resolve, reject) => { // חץ שמן
        if (isMomHappy) {
            const phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone);
        } else {
            const reason = new Error('mom is not happy');
            reject(reason);
        }

    }
);

// בטחון שני
const showOff = function (phone) {
    const message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';
    return Promise.resolve(message);
};

// קריאה לבטחון שלנו
const askMom = function () {
    willIGetNewPhone
        .then(showOff)
        .then(fulfilled => console.log(fulfilled)) // חץ שמן
        .catch(error => console.log(error.message)); // חץ שמן
};

askMom();

שימו לב שכל ה-var הוחלפו ב-const. כל ה-function(resolve, reject) הפכו לפשוטים יותר ל-(resolve, reject) =>. יש כמה יתרונות שמגיעים עם השינויים האלה.

ES7 – Async/Await

ES7 הציג async ו-await תחביר. זה מקל על התחביר האסינכרוני להבנה, מבלי ה-.then ו-.catch.

כתוב מחדש את הדוגמה שלנו עם תחביר ES7:

// ES7: דוגמה מלאה
const isMomHappy = true;

// בטחון
const willIGetNewPhone = new Promise(
    (resolve, reject) => {
        if (isMomHappy) {
            const phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone);
        } else {
            const reason = new Error('mom is not happy');
            reject(reason);
        }

    }
);

// בטחון שני
async function showOff(phone) {
    return new Promise(
        (resolve, reject) => {
            var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

            resolve(message);
        }
    );
};

// קריאה לבטחון שלנו בסגנון ES7 async await
async function askMom() {
    try {
        console.log('before asking Mom');

        let phone = await willIGetNewPhone;
        let message = await showOff(phone);

        console.log(message);
        console.log('after asking mom');
    }
    catch (error) {
        console.log(error.message);
    }
}

// async await גם כאן
(async () => {
    await askMom();
})();

הבטחות ומתי להשתמש בהם

למה אנו זקוקים להבטחות? איך נראה העולם לפני הבטחות? לפני שנענה על שאלות אלה, בואו נחזור ליסודות.

פונקציה רגילה מול פונקציה אסינכרונית

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

פונקציה רגילה לחיבור שני מספרים

// מוסיפים שני מספרים באופן רגיל

function add (num1, num2) {
    return num1 + num2;
}

const result = add(1, 2); // אתה מקבל תוצאה = 3 מיידית
פונקציה אסינכרונית לחיבור שני מספרים
// מוסיפים שני מספרים מרוחק

// מקבלים את התוצאה על ידי קריאה ל-API
const result = getAddResultFromServer('http://www.example.com?num1=1&num2=2');
// אתה מקבל תוצאה = "לא מוגדר"

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

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

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

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

// הוסף שני מספרים מרוחקים
// קבל את התוצאה על ידי קריאה ל-API

function addAsync (num1, num2, callback) {
    // השתמש ב-API הקולבק של getJSON המפורסם של jQuery
    return $.getJSON('http://www.example.com', {
        num1: num1,
        num2: num2
    }, callback);
}

addAsync(1, 2, success => {
    // קולבק
    const result = success; // אתה מקבל result = 3 כאן
});

פעולה אסינכרונית נוספת

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

// הוסף שני מספרים באופן רגיל

let resultA, resultB, resultC;

 function add (num1, num2) {
    return num1 + num2;
}

resultA = add(1, 2); // אתה מקבל resultA = 3 מיידית
resultB = add(resultA, 3); // אתה מקבל resultB = 6 מיידית
resultC = add(resultB, 4); // אתה מקבל resultC = 10 מיידית

console.log('total' + resultC);
console.log(resultA, resultB, resultC);

כך זה נראה עם קריאות חזרה:

// להוסיף שני מספרים מרוחקים
// לקבל את התוצאה על ידי קריאה ל-API

let resultA, resultB, resultC;

function addAsync (num1, num2, callback) {
    // להשתמש ב-API של קריאת חזרה מפורסם של jQuery getJSON
	// https://api.jquery.com/jQuery.getJSON/
    return $.getJSON('http://www.example.com', {
        num1: num1,
        num2: num2
    }, callback);
}

addAsync(1, 2, success => {
    // קריאת חזרה 1
    resultA = success; // אתה מקבל כאן result = 3

    addAsync(resultA, 3, success => {
        // קריאת חזרה 2
        resultB = success; // אתה מקבל כאן result = 6

        addAsync(resultB, 4, success => {
            // קריאת חזרה 3
            resultC = success; // אתה מקבל כאן result = 10

            console.log('total' + resultC);
            console.log(resultA, resultB, resultC);
        });
    });
});

דוגמא: https://jsbin.com/barimo/edit?html,js,console

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

הימנעות מקריאות חזרה מקוננות עמוקה

הבטחות יכולות לעזור לך להימנע מקריאות חזרה מקוננות עמוקה. בואו נביט בגרסת הבטחה של אותה דוגמה:

// הוספת שני מספרים מרוחקים באמצעות observable

let resultA, resultB, resultC;

function addAsync(num1, num2) {
    // שימוש ב-API של fetch של ES6, שמחזיר הבטחה
	// מה זה .json()? https://developer.mozilla.org/en-US/docs/Web/API/Body/json
    return fetch(`http://www.example.com?num1=${num1}&num2=${num2}`)
        .then(x => x.json()); 
}

addAsync(1, 2)
    .then(success => {
        resultA = success;
        return resultA;
    })
    .then(success => addAsync(success, 3))
    .then(success => {
        resultB = success;
        return resultB;
    })
    .then(success => addAsync(success, 4))
    .then(success => {
        resultC = success;
        return resultC;
    })
    .then(success => {
        console.log('total: ' + success)
        console.log(resultA, resultB, resultC)
    });

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

משתנים תצוגה

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

בואו נביט בדוגמה זהה שנכתבה עם משתנים תצוגה. בדוגמה זו, נשתמש RxJS עבור המשתנים תצוגה.

let Observable = Rx.Observable;
let resultA, resultB, resultC;

function addAsync(num1, num2) {
    // שימוש ב-API של fetch של ES6, שמחזיר הבטחה
    const promise = fetch(`http://www.example.com?num1=${num1}&num2=${num2}`)
        .then(x => x.json());

    return Observable.fromPromise(promise);
}

addAsync(1,2)
  .do(x => resultA = x)
  .flatMap(x => addAsync(x, 3))
  .do(x => resultB = x)
  .flatMap(x => addAsync(x, 4))
  .do(x => resultC = x)
  .subscribe(x => {
    console.log('total: ' + x)
    console.log(resultA, resultB, resultC)
  });

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

...

addAsync(1,2)
  .delay(3000) // עיכוב של 3 שניות
  .do(x => resultA = x)
  ...

אתה יכול לקרוא אחד מהפוסטים שלי על RxJs כאן.

סיכום

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

Source:
https://www.digitalocean.com/community/tutorials/understanding-javascript-promises