简体   繁体   中英

jQuery When Not Working as expected with Deferred Promises

So I have done quite a bit of troubleshooting on this and am banging my head against a wall. For the most part, I'm pretty familiar with promises and how they work, and have used them on a few projects. I'm having a little trouble with getting all my promises to finish in making several calls for different calendar data from the Google Calendar API and for the script to calculate the length of the resulting array to use in callback functions. Here is the relevant code:

(function($){
    var baseUrl = 'https://www.googleapis.com/calendar/v3/calendars/',
        apiKey = 'xxxxxxxxxxxx',
        calendarData = [],
        events = [],
        allCalendars = [],
        eventsToDisplay = 9;

    /* Get all the calendars that we have registered */
    function getCalendars() {
        return $.getJSON(ajaxurl, {action: 'wps_get_calendars'});
    }

    /*  Get the events for a calendar by the calendar ID */
    function getCalendarEvents(calendarID) {
        return $.getJSON(baseUrl + calendarID + '/events', {
            maxResults: '4',
            orderBy: 'startTime',
            timeMin: moment().format(),
            singleEvents: true,
            key: apiKey
        }).success(function(data){
            calendarData.push(data.items);
    });

    /*  Create a collection of promises */
    var promises = getCalendars().then(function(calendars){
        var deferreds = [];
        calendars.forEach(function(calendar){
            deferreds.push( 
                getCalendarEvents(calendar.googleCalendarId)
            );
        });
        return deferreds;
    });

    /*  Wait until all the promises have completed, then sort the events */
    $.when.apply($, promises).then(concatEvents);

})(jQuery);

Basically the issue is at the very last call to $.when as I am waiting for the array of promises I have to complete. $.when doesn't seem to be working since if I try to log the calendarData array to the console in the $.when callback, it returns an array that doesn't have a calculated length. The only way I've been able to figure out how to do this is to use setTimeout in the callback, and set it to around 2000 milliseconds, but this isn't ideal since depending on network connectivity, API availability, etc., the time to receive all the data could be completely different.

Just as an idea of what I'm seeing in the console, I get this "length-less" array when I try to log the result in the $.when callback, which can't be iterated through because the script seems to think it's empty:

没有长度的数组

Any idea what I'm doing wrong here? Thanks in advance for any help.

You had several structural issues in how your code was working. Issues I identified:

  1. Don't use the deprecated .success() . Use .then() .
  2. You were trying to return an array of promises from a promise. Instead, use $.when() there to return a single promise that resolves to an array of results.
  3. $.when() is pretty wonky in how it returns results. It returns them as separate arguments, not as an array of results (it does not work like the ES6 standard Promise.all() does).
  4. jQuery Ajax calls resolve to three arguments and interact very strangely with $.when() if you use jQuery ajax promises directly with $.when() .
  5. You were relying on side effects in higher scoped variables rather than just resolving promises with the desired data (no need for higher scoped variables to keep track of data). This makes code a lot more self-contained and resuable and not subject to race conditions (if used in more than one place).
  6. Restructure getCalendar() so it returns a single promise that resolves to an array of calendars, thus making it much simpler to use.

This should get you the results you want and it gets rid of some of the higher scoped variable and side effects you were relying on.

(function($){
    var baseUrl = 'https://www.googleapis.com/calendar/v3/calendars/',
        apiKey = 'xxxxxxxxxxxx',
        events = [],
        eventsToDisplay = 9;

    /* Get all the calendars that we have registered */
    function getCalendars() {
        return $.getJSON(ajaxurl, {action: 'wps_get_calendars'}).then(function(calendars){
            var promises = calendars.map(function(calendar) {
                return getCalendarEvents(calendar.googleCalendarId);
            });
            return $.when.apply($, promises).then(function() {
                // convert arguments to a single array as our resolved value
                return [].slice.call(arguments);
            });
        });
    }

    /*  Get the events for a calendar by the calendar ID */
    function getCalendarEvents(calendarID) {
        return $.getJSON(baseUrl + calendarID + '/events', {
            maxResults: '4',
            orderBy: 'startTime',
            timeMin: moment().format(),
            singleEvents: true,
            key: apiKey
        }).then(function(data){
            // make resolved value be just data.items
            return data.items;
    });

    /*  get all calendars */
    getCalendars().then(function(calendars){
        // process array of calendars here
        console.log(calendars);
    });

})(jQuery);

In addition, it is very easy to get confused by how $.when() works. Here's some general info to explain it.

$.when() does not resolve to an array of results. Instead, it passes each result as a separate argument to the callback. So, if you have three results, then it does this:

$.when(p1, p2, p3).then(function(r1, r2, r3) {
    console.log(r1, r2, r3);
});

Then, on top of that a jQuery Ajax call doesn't resolve to a single value either, it resolves to three values. So, when you pass N jQuery ajax promises to $.when() you get, N arguments to your callback where each argument is an array of three values (the three values from a jQuery Ajax promise).


So, if you want to process an arbitrary number of promise results in $.when() , you have to use the arguments object passed to your callback and iterate that.

function processWhenResults() {
    for (var i = 0; i < arguments.length; i++) {
        // output each ajax result
        // arguments[i] is an array of results for each corresponding ajax call
        // arguments[i][0] is the actual Ajax result value
        console.log(arguments[i][0]);
    }
}


$.when.apply($, promises).then(processWhenResults);

Or, you can do as I did in my suggested code above and convert the arguments object to an array of results so you can use normal array functions on it.


In addition, .success() has been deprecated. You should not use it. Use .then() instead.

Haven't played with them for a bit but you should be able to take this and run with it.

/*  Create a collection of promises */
var control = $.Deferred();
var deferreds = [];
getcalendars().done(function(calendars){
    calendars.forEach(function(calendar){
        deferreds.push( 
            getCalendarEvents(calendar.googleCalendarId)
        );
    });
    control.resolve();
});

control.done(function(){
  /*  Wait until all the promises have completed, then sort the events */
  $.when.apply($, deferreds).done(function() {concatEvents});    })

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM