הבטחות לעתיד: תכנות אסינכרוני ב- JavaScript עם Promises

12 בדצמבר 2011

no comments

תכנות אסינכרוני ב- JavaScript עם Promisesתכנות אסינכרוני הופך פופולרי יותר ויותר באפליקציות ה- Web, אך מצד שני, ב- JavaScript הוא אינו קל לביצוע כיום. ספריות ה- JavaScript הפופולריות (כמו jQuery, Dojo ועוד) הוסיפו רמת אבסטרקציה בשם Promise (לפעמים נקראת deferred) כדי להפוך את התכנות האסינכרוני ב- JavaScript לפשוט יותר.
בפוסט זה נראה איך להשתמש ב- Promises באפליקציות Web. נדגים את השימוש ב- Promisses עם XMLHttpRequest2 (בקיצור XHR2).

תכנות אסינכרוני: למה זה כ”כ קשה?

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

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

function searchTwitter(term, onload, onerror) {
    var xhr, results, url;
    url = 'http://search.twitter.com/search.json?rpp=100&q=' + term;
    xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.onload = function (e) {
        if (this.status === 200) {
            results = JSON.parse(this.responseText);
            onload(results);
        }
    };

    xhr.onerror = function (e) {
        onerror(e);
    };
    xhr.send();
}

function handleError(error) {
    /* handle the error */
}

function concatResults() {
    /* order tweets by date */
}

function loadTweets() {
    var container = document.getElementById('container');

    searchTwitter('#IE10', function (data1) {
        searchTwitter('#IE9', function (data2) {

            /* Reshuffle due to date */
            var totalResults = concatResults(data1.results, data2.results);
            totalResults.forEach(function (tweet) {
                var el = document.createElement('li');
                el.innerText = tweet.text;
                container.appendChild(el);
            });
        }, handleError);
    }, handleError);
}

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

הכירות עם Promises להקלת התכנות האסינכרוני ב- JavaScript

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

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

var results = searchTwitter(term).then(filterResults);
displayResults(results);

בכל רגע נכון, Promise יכול להיות באחד משלושה מצבים: unfulfilled, resolved או rejected.

איך זה ממומש?

כדי להבין איך ממומש רעיון ה- Promise, נתחיל מ- CommonJS Promise/A. הפונקציה then של האובייקט Promise מוסיפה handlers לטיפול במצבי resolved ו- rejected. הפונקציה מחזירה Promise נוסף כדי לאפשר שרשור של Promises ובכך לאפשר למפתחים לשרשר קריאות אסינכרוניות בהן תוצאה של הקריאה הראשונה עוברת לשניה וכו’.

then(resolvedHandler, rejectedHandler);

פונקציית ה- callback ששמה resolvedHandler נקראת כאשר ה- Promise עובר למצב resolved והפונקציה rejectedHandler נקראת כאשר ה- Promise עובר למצב rejected.

כדי לממש Promise נתחיל באובייקט פשוט:
var Promise = function () {
    /* initialize promise */
};

כעת, נממש את פונקציית ה- then המקבלת פונקציות לטיפול במצבי ה- Promise ומאפשרת לשרשר Promises.

Promise.prototype.then = function (onResolved, onRejected) {
    /* invoke handlers based upon state transition */
};

נשמור גם את הפונקציות לטיפול בשני המצבים של ה- Promise:

Promise.prototype.resolve = function (value) {
    /* move from unfulfilled to resolved */
};

Promise.prototype.reject = function (error) {
    /* move from unfulfilled to rejected */
};

דוגמה לשימוש ב- Promises ליצירת קוד JavaScript אסינכרוני

נשנה את הדוגמה למעלה שפונה לשירות של טוויטר ומחפשת ציוצים עם התגית #IE10. הפונקציה searchTwitter משתמשת ב- XMLHttpRequest2 לטוויטר ועוטפת את הקריאה ב- Promise.

הפונקציה loadTweets קוראת לפונקציה searchTwitter, מקבלת בחזרה Promise ורושמת פונקציה להמשך ביצוע כאשר התוצאות חוזרות.
function searchTwitter(term) {
    var url, xhr, results, promise;
    url = 'http://search.twitter.com/search.json?rpp=100&q=' + term;
    promise = new Promise();
    xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);

    xhr.onload = function (e) {
        if (this.status === 200) {
            results = JSON.parse(this.responseText);
            promise.resolve(results);
        }
    };

    xhr.onerror = function (e) {
        promise.reject(e);
    };
    xhr.send();
    return promise;
}

function loadTweets() {
    var container = document.getElementById('container');
    searchTwitter('#IE10').then(function (data) {
        data.results.forEach(function (tweet) {
            var el = document.createElement('li');
            el.innerText = tweet.text;
            container.appendChild(el);
        });
    }, handleError);
}
כעת, כשאנחנו יודעים לבצע קריאה ססינכרונית אחת כ- Promise, נרצה לראות איך לבצע יותר מקריאה אחת ולתאם בין הקריאות. לצורך כך ניצור את הפונקציה ()when על אובייקט ה- Promise שמטפלת בקריאות האסינכרוניות לפי תור.
Promise.when = function () {
    /* handle promises arguments and queue each */
};

ועכשיו, אנחנו יכולים סוף סוף לבצע מספר קריאות אסינכרוניות בו זמנית. למשל, נחפש בטוויטר גם את התגית #IE10 וגם את התגית #IE9, ונשתמש בפונקציה ()when.

function loadTweets() {
    var container, promise1, promise2;
    container = document.getElementById('container');
    promise1 = searchTwitter('#IE10');
    promise2 = searchTwitter('#IE9');
    Promise.when(promise1, promise2).then(function (data1, data2) {
        /* Reshuffle due to date */
        var totalResults = concatResults(data1.results, data2.results);
        totalResults.forEach(function (tweet) {
            var el = document.createElement('li');
            el.innerText = tweet.text;
            container.appendChild(el);
        });
    }, handleError);
}

דוגמא: שימוש ב- Promises ב- jQuery

המימוש ב- jQuery מעט שונה מהמימוש ב- CommonJS, ונקרא Deferred. ב- jQuery הפונקציה then מקבלת 2 handlers: גם את זה של ההצלחה וגם את זה של הכשלון. לדוגמא:

function loadTweets() {
    $.ajax({
        url: 'http://search.twitter.com/search.json',
        dataType: 'jsonp',
        data: { q: '#IE10', rpp: 100 }
    }).then(function (data) {
        /* handle data */
    }, function (error) {
        /* handle error */
    });
}

סיכום

בעזרת Promises, מפתחים יכולים לפתח אפליקציות Web מורכבות יותר המספקות חווית משתמש טובה יותר.אימוץ הגישה ע”י ספריות ה- JavaScript הפופולריות הופכות את השימוש ב- Promises לנוחה ופשוטה ליישום.

תהנו!

Add comment
facebook linkedin twitter email