delay for jquery promises

4k views Asked by At

I cannot find delay or wait function for jQuery promises. I have found one function on the SO (Using jQuery.Deferred to avoid nested setTimeout callbacks):

function delay(time) {
    return function () {
        console.log("Delaying");
        var ret = new $.Deferred();
        setTimeout(function () {
            ret.resolve();
        }, time);
        return ret;
    };
}

And, it's the way how I use it:

 run: function () {
        return $()
            .promise()
            .then(function () {
                console.log("call together");
                console.log("call together");    
            })
            .then(delay(2000))
            .then(function () {
                console.log("call first");
            })
            .then(delay(2000))
            .then(function () {
                console.log("call second");
            })
    }

I want to extend promise or deferred object that I can write like:

run: function () {
            return $()
                .promise()
                .then(function () {
                    console.log("call together");
                    console.log("call together");    
                })
                .delay(2000)
                .then(function () {
                    console.log("call first");
                })
                .delay(2000)
                .then(function () {
                    console.log("call second");
                })
        }
3

There are 3 answers

1
Roamer-1888 On BEST ANSWER

As @Bergi says jQuery Deferreds/Promises are not extendible by prototypal inheritance.

Instead, the model adopted by jQuery is to allow individual Promise instances to be extended with the syntax :

deferred.promise(target);
//or, 
promise.promise(target); //(though the documentation doesn't make this clear)
// where `target` is an "object onto which the promise methods have to be attached"
// see https://api.jquery.com/deferred.promise/

By defining a constructor with a bunch of methods, any jQuery Deferred or Promise can be extended with the simple syntax

.promise(Constructor())

In my unpublished, undocumented jQuery promises Playground, the constructor is named $P and kept in the jQuery namespace, hence the actual syntax I use is :

.promise($.$P())

You need to be aware of that, for the most part, it's not necessary to call $.$P() explicitly as the Playground includes a $.when_() method that returns an already extended Promise.

Here's an abbreviated version of the Playground with just enough to provide a .delay() method :

(function($) {
    /* ***********************************
     * The $.$P function returns an object
     * designed to be extended with 
     * promise methods using the syntax :
     *    myDeferred.promise($.$P())
     *    myPromise.promise($.$P())
     * where `myDeferred`/`myPromise` 
     * are jQuery Deferred/Promise objects.
     * ***********************************/

    /* ***********************************
     * Methods
     * ***********************************/
    $.$P = function() {
        if (this instanceof $.$P) {
            return this;
        } else {
            return new $.$P();
        }
    };
    $.$P.prototype.then_ = function(fa, fb) {
        /* A promise method that is the same as .then()
         * but makes these extra methods available 
         * down-chain.
         */
        return this.then(fa||null, fb||null).promise($.$P());
    }
    $.$P.prototype.delay_ = function(ms) {
        /* A promise method that 
         * introduces a down-chain delay.
         */
        var promise = this;
        function f(method) {
            return function() { setTimeout(function(){ method.apply(null,this); }.bind(arguments), ms||0); };
        }
        return $.Deferred(function(dfrd) { 
            promise.then(f(dfrd.resolve), f(dfrd.reject));
        }).promise($.$P());
    }

    /* ***********************************
     * Utility functions
     * ***********************************/
    function consolidate(args) {
        /* Convert mixed promises/arrays_of_promises to single array.
         * Called by all the when_() methods below.
         */
        return Array.prototype.slice.apply(args).reduce(function(arr, current) {
            return arr.concat(current);
        }, []);
    }

    /* ***********************************
     * This section extends the jQuery namespace 
     * with a "jQuery.when_()" method.
     * ***********************************
     */
    $.extend({
        'when_': function() {
            return $.when.apply(null, consolidate(arguments)).promise($.$P()).then_(function() {
                return consolidate(arguments);
            });
        },
    });
})(jQuery);

The full Playground also includes a whole bunch more static and promise-instance methods for other purposes, and developing them is the essence of the play.

The ground-rules for using the Playgound are as follows :

  • All the Playground's static and promise methods end in "_" underscore.
  • Static methods, eg $.when_(), are made available just by installing the Playgound.
  • Promises in a promise chain are extended by including a static method, eg .when_(), or chaining .promise($.$P()).
  • In a promise chain, the extensions remain available (down the chain) by using "..._" methods rather than the standard methods, eg .then_() in place of .then().

So here's how to use it to impose the delays required by the question :

jQuery(function($) {
    var MYNAMESPACE = {
        run: function (t) {
            return $.when_()
            .then_(function () {
                log("call together");
                log("call together");    
            })
            .delay_(t)
            .then_(function () {
                log("call first");
            })
            .delay_(t)
            .then_(function () {
                log("call second");
            });
        }
    }
});

DEMO

In the demo, the button's click handler gives further indication of how the Playground can be used.

Provisos on using the Playground :

  • As I say - it's a playground.
  • As an adaptor for jQuery, not a patch, it is horribly inefficient in places. Worst aspect is that some methods create an intermediate promise in addition to the one they return.
  • Not tested to the standards required for use in production code, so use with caution.

And lastly, only consider the above if you are determined to implement delay with jQuery. It's far far simpler to use a promise lib that already has a .delay() method.

4
guest271314 On

Edit, Updated

Try adding property delay to jQuery.Deferred

    delay: function(t) {
      return this.then(function() {
        var args = arguments;
        return new $.Deferred(function(d) {
          setTimeout(function() {
            // return `data`, if any, to next method, e.g., `.then`, in chain
            d.resolveWith(this, args)
          }.bind(this), t || 0)
        }).promise()
      })
    }

(function($) {
  $.Deferred = function(a) {
    var b = [
        ["resolve", "done", $.Callbacks("once memory"), "resolved"],
        ["reject", "fail", $.Callbacks("once memory"), "rejected"],
        ["notify", "progress", $.Callbacks("memory")]
      ],
      c = "pending",
      d = {
        delay: function(t) {
          return this.then(function() {
            var args = arguments;
            return new $.Deferred(function(d) {
              setTimeout(function() {
                // return `data`, if any, to next method, e.g., `.then`, in chain
                d.resolveWith(this, args)
              }.bind(this), t || 0)
            }).promise()
          })
        },
        state: function() {
          return c
        },
        always: function() {
          return e.done(arguments).fail(arguments), this
        },
        then: function() {
          var a = arguments;
          return $.Deferred(function(c) {
            $.each(b, function(b, f) {
              var g = $.isFunction(a[b]) && a[b];
              e[f[1]](function() {
                var a = g && g.apply(this, arguments);
                a && $.isFunction(a.promise) 
                ? a.promise()
                  .done(c.resolve)
                  .fail(c.reject)
                  .progress(c.notify) 
                : c[f[0] + "With"](this === d 
                  ? c.promise() 
                  : this, g ? [a] : arguments)
              })
            }), a = null
          }).promise()
        },
        promise: function(a) {
          return null != a ? $.extend(a, d) : d
        }
      },
      e = {};
    return d.pipe = d.then, $.each(b, function(a, f) {
      var g = f[2],
        h = f[3];
      d[f[1]] = g.add, h && g.add(function() {
        c = h
      }, b[1 ^ a][2].disable, b[2][2].lock), e[f[0]] = function() {
        return e[f[0] + "With"](this === e ? d : this, arguments), this
      }, e[f[0] + "With"] = g.fireWith
    }), d.promise(e), a && a.call(e, e), e
  }

}(jQuery));

var p = {
  run: function() {
    return $()
      .promise()
      .then(function() {
        console.log("call together");
        console.log("call together");
        // do stuff
        // pass `data` to next `.then`
        return "call first";
      })
      .delay(2000)
      .then(function(data) {
        console.log(data);
      })
      .delay(2000)
      .then(function() {
        console.log("call second");
      })
  }
};

p.run();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js">
</script>

0
THX-1138 On

Here is my solution. I wrap $.Deferred(afterBuild) and wrap original afterBuild that in turn wraps .promise(obj) method, extending given obj with custom delay method. Which uses window.setTimeout:

Note: it only delays done branch.

function extendPromises(extensions) {
    $.Deferred = (function (originalDeferred) {
        return function (afterBuild) {
            var newAfterBuild = function (d) {
                d.promise = (function (originalPromise) {
                    return function (obj) {
                        return originalPromise.call(this, $.extend(obj, extensions));
                    };
                })(d.promise);
                if (afterBuild) afterBuild.apply(this, arguments);
                return this;
            };
            return originalDeferred.call(this, newAfterBuild);
        };
    })($.Deferred);
}

extendPromises({
    delay: function (delay) {
        return this.then(function (value) {
            var d = $.Deferred();
            window.setTimeout(function () {
                d.resolve(value);
            }, delay);
            return d.promise();
        });
    }
});

// so now I can do:
$.when("hello")
.then(function (value) { $("#log").append(value+"\n"); return value; })
.delay(1000)
.then(function (value) { $("#log").append(value); return value; });
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<textarea id="log" rows=5></textarea>