jQuery嵌套了每个/ Ajax问题

I have nested each loops but an Ajax request in the second loop seems to not be adding to a variable that I declare in the first loop.

This is an example ("personal" information excluded) of what I am using:

var pages = [["Page Title 1", ["Page URL 1", "Page URL 2", "Page URL 3"]], ["Page Title 2", ["Page URL 1", "Page URL 2", "Page URL 3"]], ["Page Title 3", ["Page URL 1", "Page URL 2", "Page URL 3"]]];

function loadFeeds() {
$.each(pages, function(index, page) {
    $(".pages").append("<a class=\"pagename\" href=\"#" + page[0] + "\">" + page[0] + "</a>");
    html = "<div class=\"page\" id=\"" + page[0] + "\">";
    $.each(page[1], function(index, feedUrl) {

    $.ajax({
    type: "GET",
    url: feedUrl,
    processData : true,
    data: {
        tagmode: "any"
        },
    jsonp: "jsonp",
    dataType: "jsonp",
    success: function(data) {
        html += "Some header HTML";

        $.each(data.responseData.feed.entries, function(i,entry) {
            if (entry.image_urls.length === 0) {
                html += "HTML from the data";
            }
            else {
                html += "More HTML";
            }

            if ( i == 34 ) return false;
        });
        html += "Closing tags from the header";
        afterAjax();
        // console.log(html); returns correct information here
    },
            error: function(x,y,z) {
                alert(x.responseText);
            }
        });
    // console.log(html); returns everything declared to html OUTSIDE of the ajax request
    });
        $("body").append(html + "</div>");
    // Also tried: $("body").ajaxStop(function(){ $(this).append(html + "</div>"); }); because Ajax is asynchronous
});
}​

Any ideas as to what is going on?

EDIT:

Full non-working demo, WIP page functionality: http://jsfiddle.net/SO_AMK/u42uy/

Full working demo, no page functionality:

Full screen: http://jsfiddle.net/SO_AMK/LXkaN/embedded/result/
Normal: http://jsfiddle.net/SO_AMK/LXkaN/

Please note that these are without images and are not the full app.

Do the follow:

  • Add var html = '' after var pages (line 1);
  • Change html = to html += (line 6);
  • Add html = '' after $("body").append(html + "</div>"); (line 41).

Result:

var pages = [["Page Title 1", ["Page URL 1", "Page URL 2", "Page URL 3"]], ["Page Title 2", ["Page URL 1", "Page URL 2", "Page URL 3"]], ["Page Title 3", ["Page URL 1", "Page URL 2", "Page URL 3"]]];
var html = ''; // ADD THIS

function loadFeeds() {
$.each(pages, function(index, page) {
    $(".pages").append("<a class=\"pagename\" href=\"#" + page[0] + "\">" + page[0] + "</a>");
    html += "<div class=\"page\" id=\"" + page[0] + "\">"; //ADD THE +

    $.each(page[1], function(index, feedUrl) {

    $.ajax({
    type: "GET",
    url: feedUrl,
    processData : true,
    data: {
        tagmode: "any"
        },
    jsonp: "jsonp",
    dataType: "jsonp",
    success: function(data) {
        html += "Some header HTML";

        $.each(data.responseData.feed.entries, function(i,entry) {
            if (entry.image_urls.length === 0) {
                html += "HTML from the data";
            }
            else {
                html += "More HTML";
            }

            if ( i == 34 ) return false;
        });
        html += "Closing tags from the header";
        afterAjax();
    },
            error: function(x,y,z) {
                alert(x.responseText);
            }
        });
    });
        $("body").append(html + "</div>");
        html = ''; // RESET HTML TO NEXT Each
});

Unless your ajax calls are synchronous (and I see no signs that they are), your each loop will finish and return BEFORE the ajax calls success functions run. You CANNOT structure your code this way. That means that $("body").append(html + "</div>"); will run before the ajax calls get a chance to add anything to your html variable.

You need to think about ajax response callbacks as independent pieces of code that run on their own timing sometime after your .each() loops are done and design the handling of the responses with that assumption in mind. Also keep in mind that multiple ajax calls aren't even guaranteed to complete in the order they were sent.

You could order the ajax calls by sending one and not sending the next until that one completes, keeping the combined html in a string variable like you have and then in the completion function of the last ajax call, you append the final accumulated html string to the body.

Here's another problem. At the start of your first .each() loop, you have this;

html = "<div class=\"page\" id=\"" + page[0] + "\">";

That reinitailizes the html variable, overwriting any results that might already be in there. You need to initialize it once before any .each() loop.

The Issue

Looks like a race condition. Remember that AJAX runs asynchronously and thusly, the success handler will run at some point in the future after the AJAX response has been received successfully. While the AJAX request is dispatched the rest of the script is executing. Specifically speaking:

$("body").append(html + "</div>");

Is running before the success handler is fired; meaning your html variable has not been updated at all yet.

Solution 1

Move

$("body").append(html + "</div>");

Within your success handler.

    html += "Closing tags from the header";
    afterAjax();
    // console.log(html); returns correct information here
    $("body").append(html + "</div>");
},

Solution 2 ($.ajaxStop)

Ensure that you register your handler before your make your $.ajax() calls. This will ensure your AJAX requests don't fire and finish before your code that registers your handler for .ajaxStop.

function loadFeeds() {
    html = "";
    $.ajaxStop(function(){$(document.body).append(html + "</div>);

    $.each(pages, function(index, page) {
        $(".pages").append("<a class=\"pagename\" href=\"#" + page[0] + "\">" + page[0] + "</a>");
        html = "<div class=\"page\" id=\"" + page[0] + "\">";
    ...
}

Solution 3 (From your jsLint+Deffereds)

This example is taken from your fully working jsLint link. This code takes all of the $.ajax requests being created in your loop and stores in them in an arrary to be latter applied to a $.when().done() call. $.when() will inspect each $.ajax request applied to it and when it is done, call the handler specified in $.done(). Each $.ajax request stores its response in the html = [] array that is defined at the start of $.ready(). When the defered check ($.when()) sees all the $.ajax requests have completed we then join the array (array.join('')) to make 1 big HTML block and append it to the document. We then call your afterAjax() function.

$(document).ready(function () {
    var html = [];
    var feeds = ["http://pulsesubscriber.appspot.com/items?feed=http://feeds.gawker.com/lifehacker/vip&jsonp=?",
                 "http://pulsesubscriber.appspot.com/items?feed=http://allthingsd.com/feed&jsonp=?",
                 "http://pulsesubscriber.appspot.com/items?feed=http://feeds.cnet.com/latestnews/pulse&jsonp=?"];
    loadFeeds(feeds);

    function loadFeeds(feeds) {
        var requests = [];

        $.each(feeds, function(index, feedUrl) {
            requests.push($.ajax({
                type: "GET",
                url: feedUrl,
                processData : true,
                data: {
                    tagmode: "any"
                    },
                jsonp: "jsonp",
                dataType: "jsonp",
                success: function(data) {
                    var feedElements = "<header class=\"feed-title\"><div class=\"feed-title-content\"><span class=\"feed-title-text\">" +
                        data.responseData.feed.title +
                        "</span></div></header><section class=\"row\"><div class=\"scroll-left\"></div><div class=\"row-scroll\">";

                    $.each(data.responseData.feed.entries, function(index,entry) {
                        var feedElements = '';
                        if (entry.image_urls.length === 0) {
                            feedElements += "<div class=\"tile no-img\"><title class=\"tile-title no-img\">" +
                                entry.title +
                                "</title><hr class=\"no-img hr\" /><span class=\"no-img excerpt\">" +
                                entry.contentSnippet +
                                "</span><div class=\"tile-modal\"><div class=\"article-wrapper\">
<div class=\"article-header\">
<a class=\"article-title-link\" target=\"_blank\" href=\"" +
                                entry.link +
                                "\">
<h1 class=\"article-title\">" +
                                entry.title +
                                "</h1>
</a>
<h2 class=\"article-byline\">By " +
                                entry.author +
                                ": " +
                                data.responseData.feed.title +
                                "</h2>
</div>
<hr class=\"article-hr\"/>
<div class=\"article-content\">" +
                                entry.content +
                                "
<a class=\"read-more\" target=\"_blank\" href=\"" +
                                entry.link +
                                "\">Read More...</a>
</div>
</div></div></div>
";
                        }
                        else {
                                feedElements += "<div class=\"tile\"><img class=\"tile-image\" src=\"" +
                                entry.image_urls[0] +
                                "\" /><title class=\"tile-title\">" +
                                entry.title +
                                "</title><div class=\"tile-modal\"><div class=\"article-wrapper\">
<div class=\"article-header\">
<a class=\"article-title-link\" target=\"_blank\" href=\"" +
                                entry.link +
                                "\">
<h1 class=\"article-title\">" +
                                entry.title +
                                "</h1>
</a>
<h2 class=\"article-byline\">By " +
                                entry.author +
                                ": " +
                                data.responseData.feed.title +
                                "</h2>
</div>
<hr class=\"article-hr\"/>
<div class=\"article-content\">" +
                                entry.content +
                                "
<a class=\"read-more\" target=\"_blank\" href=\"" +
                                entry.link +
                                "\">Read More...</a>
</div>
</div></div></div>
";
                        }
                        html.push(feedElements);
                        console.log('updated elements');
                        if(index == 34 ){
                            return false;
                        }
                    });
                },
                error: function(x,y,z) {
                    console.log(x.responseText);
                }
            }));
        });

        $.when.apply(this,requests).done(function(){
            console.log('when!');
            console.log(html);
            $(document.body).append($(html.join('')));
            afterAjax();
        });

    }



    $("#refresh-all").click(function(){
        $("body > header, body > section").remove();
        $("body").removeClass("active");
        loadFeeds();
    });

    function afterAjax() {
            $(".page:first").addClass("active");
            $(".tile").click(function(){
            if ($(".tile-modal.tile-modal-active").length) {
                $(".tile-modal.tile-modal-active").removeClass("tile-modal-active");
                $(this).children(".tile-modal").addClass("tile-modal-active");
            }
            else {
                $(this).children(".tile-modal").addClass("tile-modal-active");
                $("body").addClass("active");
            }
        });


        $(".scroll-left").hover(function(){
            $(this).parent().animate({scrollLeft: 0}, 7000);
        }, function() {
            $(this).parent().stop();
        });

        $(".scroll-right").hover(function(){
            $(this).parent().animate({scrollLeft: $(this).siblings(".row-scroll").width()}, 7000);
        }, function() {
            $(this).parent().stop();
        });
    }
});

If you would like to execute a series of requests and once they have all executed do another action, you can utilize Futures & Promises Pattern or deffereds. In jQuery this is done in via .promise()(Futures/Promises) or .when() (Deffered).

A Note on Global Variables

It is also worth noting that not use var html = ... makes your variable a global; which is generally frowned upon. It would be best once moving $("body").append(html+"</div>") within the success handler to also declare html as var html.

ajaxStop is not firing because jsonp requests are by default not global. See here

I've updated your fiddle that forces jsonp requests as global. http://jsfiddle.net/u42uy/3/

*edit typo