jQuery deferred promises executing out of order?

2019-08-07 16:22发布

问题:

EDIT: IMPORTANT NOTE this is using jQuery 1.7.2, and no it cannot be changed from this version

I'm new to promises and trying to wrap my head around them. I'm trying to execute a series of functions in order, waiting for them to complete before creating some child views (this is in Backbone.js). Here's my code:

initialize: function () {
    console.log('AppView::initialized!');
    var _this = this;

    $.when( _this.processCookies() )
        .then( _this.loadAdScripts() )
        .then( _this.createChildViews() );
},

processCookies: function () {
    var def = $.Deferred();
    console.log('(1) PROCESS COOKIES');
    return def.resolve();
},


/**
 * Instantiates new instances of the child views.
 */
createChildViews: function () {
    var _this = this;
    console.log('(4) CREATING CHILD VIEWS');
},

loadAdScripts: function () {

    var _this = this,
        def = $.Deferred();

    $.when(
        _this.insertScript({
            name: 'example1',
            async: false,
            src: '//www.example.com/script1.js',
        }),
        _this.insertScript({
            is_mobile: is_mobile,
            name: 'example2',
            async: true,
            src: '//example.com/script2.js'
        })
    )
    .done(function () {
        console.log('(3) ALL SCRIPTS LOADED');
        def.resolve();
    });
},

insertScript: function (script) {
    var def = $.Deferred(),
        protocol = (document.location.protocol === 'https:' ? 'https:' : 'http:');

    // dont script 2 on mobile.
    if (script.name === 'example2' && script.is_mobile) {
        console.log('skipping script');
        return def.resolve();
    }

    var promise = $.ajax({
        dataType: 'script',
        cache: false,
        async: script.async,
        url: protocol + script.src,
    });

    promise.done( function () {
        console.log('(2) SINGLE SCRIPT LOADED');
        return def.resolve();
    });


},

So, the desired flow here is:

  1. When the processCookies() function is completed,
  2. execute loadAdScripts function 2a. insertScript() fires, script 1 loads 2b. insertScript() fires, script 2 loads
  3. when BOTH scripts are finished, execute createChildViews function.

so, observing the console.log() placeholders in the code, I expect to see in my console:

'(1) PROCESS COOKIES'
'(2) SINGLE SCRIPT LOADED'
'(2) SINGLE SCRIPT LOADED'
'(3) ALL SCRIPTS LOADED'
'(4) CREATING CHILD VIEWS'

however what I actually see is:

'(1) PROCESS COOKIES'
'(3) ALL SCRIPTS LAODED'
'(4) CREATING CHILD VIEWS'
'(2) SINGLE SCRIPT LOADED'
'(2) SINGLE SCRIPT LOADED'

What is wrong with my promises, and why are they not executing in the expected order?

回答1:

$.when( _this.processCookies() )
        .then( _this.loadAdScripts() )
        .then( _this.createChildViews() );

appear to be calling loadScripts() , createChildViews() immediately, instead try referencing function names at .then(_this.loadAdScripts) callbacks .

return def.resolve() returns jQuery.Deferred() object , not jQuery promise object ; try adding .promise() after .resolve() to return jQuery promise object .

jQuery.ajax() returns jQuery promise object ; not necessary to create new jQuery $.Deferred() object, could return $.ajax()

initialize: function () {
    console.log('AppView::initialized!');
    var _this = this;

    $.when( _this.processCookies() )
        // reference function name, not invoked
        .then( _this.loadAdScripts )
        .then( _this.createChildViews );
},

processCookies: function () {
    // no asynchronous operations appear here, 
    // no need to include `$.Deferred()` or `.promise()` object
    // var def = $.Deferred();
    console.log('(1) PROCESS COOKIES');
    // return jQuery promise object, not deferred object
    // return def.resolve().promise();
},


/**
 * Instantiates new instances of the child views.
 */
createChildViews: function () {
    var _this = this;
    console.log('(4) CREATING CHILD VIEWS');
},

loadAdScripts: function () {

    //var _this = this,
    //    def = $.Deferred();

    return $.when(
        _this.insertScript({
            name: 'example1',
            async: false,
            src: '//www.example.com/script1.js',
        }),
        _this.insertScript({
            is_mobile: is_mobile,
            name: 'example2',
            async: true,
            src: '//example.com/script2.js'
        })
    )
    .done(function () {
        console.log('(3) ALL SCRIPTS LOADED');
        // def.resolve();
    });
},

insertScript: function (script) {
    // var def = $.Deferred(),
        protocol = (document.location.protocol === 'https:' ? 'https:' : 'http:');

    // dont script 2 on mobile.
    // if (script.name === 'example2' && script.is_mobile) {
    //    console.log('skipping script');
    //    return def.resolve().promise();
    // }

    var promise = script.name === 'example2' && script.is_mobile 
                  ? $.when() 
                  : $.ajax({
                      dataType: 'script',
                      cache: false,
                      async: script.async,
                      url: protocol + script.src,
                    });

    promise.done( function () {
        console.log('(2) SINGLE SCRIPT LOADED');
        // def.resolve();
    });    
},

EDIT: IMPORTANT NOTE this is using jQuery 1.7.2, and no it cannot be changed from this version

Edit, Updated

Expected order may not be possible using jQuery version 1.7.2 , without modifying source.

Appear to return expected order when using jQuery version 1.8+ , following update of deferred.then ; see http://blog.jquery.com/2012/08/09/jquery-1-8-released/ , http://bugs.jquery.com/ticket/11010

1.8.0

var dfd = {
    initialize: function () {
        console.log('AppView::initialized!');
        _this = dfd;

        $.when(_this.processCookies())
        // reference function name, not invoked
        .then(_this.loadAdScripts)
        .then(_this.createChildViews);
    },

    processCookies: function () {
        // no asynchronous operations appear here, 
        // no need to include `$.Deferred()` or `.promise()` object
        var def = $.Deferred(function (d) {
            setTimeout(function () {
                d.resolve('(1) PROCESS COOKIES')
            }, Math.floor(Math.random() * 1000));
        }).promise();
        def.then(function (msg) {
            console.log(msg);
        });
        return def.promise()
    },


    /**
     * Instantiates new instances of the child views.
     */
    createChildViews: function () {
        _this = dfd;
        console.log('(4) CREATING CHILD VIEWS');
    },

    loadAdScripts: function () {
        _this = dfd;
        return $.when.apply(_this, [_this.insertScript({
            name: 'example1',
            async: false,
            src: '//www.example.com/script1.js',
        }),
        _this.insertScript({
            is_mobile: true,
            name: 'example2',
            async: true,
            src: '//example.com/script2.js'
        })]).then(function () {
            console.log('(3) ALL SCRIPTS LOADED');
        })
    },

    insertScript: function (script) {
        // var def = $.Deferred(),
        protocol = (document.location.protocol === 'https:' ? 'https:' : 'http:');

        // dont script 2 on mobile.
        // if (script.name === 'example2' && script.is_mobile) {
        //    console.log('skipping script');
        //    return def.resolve();
        // }

        var promise = $.when( script.name === 'example2' && script.is_mobile ? $.Deferred(function (d) {
            setTimeout(function () {
                d.resolve('(2) skipping script', protocol + script.src)
            }, Math.floor(Math.random() * 1000))
        }).promise() : $.Deferred(function (d) {
            setTimeout(function () {
                d.resolve('(2) SINGLE SCRIPT LOADED', protocol + script.src)
            }, Math.floor(Math.random() * 1000))
        }).promise())
        
        return promise.then(function(msg) {
          console.log(msg)
        });
    }
};

dfd.initialize();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"></script>

1.72

var dfd = {
    initialize: function () {
        console.log('AppView::initialized!');
        _this = dfd;

        $.when(_this.processCookies())
        // reference function name, not invoked
        .then(_this.loadAdScripts)
        .then(_this.createChildViews);
    },

    processCookies: function () {
        // no asynchronous operations appear here, 
        // no need to include `$.Deferred()` or `.promise()` object
        var def = $.Deferred(function (d) {
            setTimeout(function () {
                d.resolve('(1) PROCESS COOKIES')
            }, Math.floor(Math.random() * 1000));
        }).promise();
        def.then(function (msg) {
            console.log(msg);
        });
        return def.promise()
    },


    /**
     * Instantiates new instances of the child views.
     */
    createChildViews: function () {
        _this = dfd;
        console.log('(4) CREATING CHILD VIEWS');
    },

    loadAdScripts: function () {
        _this = dfd;
        return $.when.apply(_this, [_this.insertScript({
            name: 'example1',
            async: false,
            src: '//www.example.com/script1.js',
        }),
        _this.insertScript({
            is_mobile: true,
            name: 'example2',
            async: true,
            src: '//example.com/script2.js'
        })]).then(function () {
            console.log('(3) ALL SCRIPTS LOADED');
        })
    },

    insertScript: function (script) {
        // var def = $.Deferred(),
        protocol = (document.location.protocol === 'https:' ? 'https:' : 'http:');

        // dont script 2 on mobile.
        // if (script.name === 'example2' && script.is_mobile) {
        //    console.log('skipping script');
        //    return def.resolve();
        // }

        var promise = $.when( script.name === 'example2' && script.is_mobile ? $.Deferred(function (d) {
            setTimeout(function () {
                d.resolve('(2) skipping script', protocol + script.src)
            }, Math.floor(Math.random() * 1000))
        }).promise() : $.Deferred(function (d) {
            setTimeout(function () {
                d.resolve('(2) SINGLE SCRIPT LOADED', protocol + script.src)
            }, Math.floor(Math.random() * 1000))
        }).promise())
        
        return promise.then(function(msg) {
          console.log(msg)
        });
    }
};

dfd.initialize();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>