2
\$\begingroup\$

I've wrote a very simple implementation of Deferred / Promise pattern (inspired by jQuery's $.deferred) for my tiny project (which does not use jQuery)

It can handle multiply onSuccess an onFail callbacks with result caching. Example in comments above the code.

/**
 * defer() returns methods:
 *      resolve(data) - request is successfully resolved (returns nothing)
 *      reject(msg) - request if failed (returns nothing)
 *      promise() - returns promise function
 *
 * promise() returns methods:
 *      done(successfulCallback) - add on successful callback (returns nothing)
 *      fail(failCallback) - add on fail callback (returns nothing)
 *
 * Example:
 *
 * function someHeavyRequest(url) {
 *      var d = defer();
 *      doHeavyRequest(url,
 *          function (data) {
 *              // on success
 *              // ...do something
 *              d.resolve(data);
 *          },
 *          function (data) {
 *              // on fail
 *              // ...do something
 *              d.reject(data);
 *          });
 *      return d.promise();
 * }
 *
 * var req = someHeavyRequest("http://some.url/of/request/");
 *
 * req.done(function (data) {
 *      console.log("We got it!");
 *      console.log(data);
 * });
 *
 * req.done(function (data) {
 *      console.log("We got it again without new request!");
 *      console.log(data);
 * });
 *
 * req.fail(function (data) {
 *      console.error("Something wrong");
 *      console.error(data);
 * });
 *
 */
function defer () {
    var status = 0, // 0 = in progress / 1 = successful / -1 = fail
        callbacks = { done: [], fail: [] },
        args = { resolve: [], reject: [] },
        execute = function (callbacks, args) {
            var f;
            while (callbacks.length) {
                f = callbacks.shift();
                if (f) f.apply(this, args);
            }
        };

    return {
        promise: function () {
            return {
                done: function (callback) {
                    callbacks.done.push(callback);
                    if (status === 1) {
                        execute(callbacks.done, args.resolve);
                    }
                },

                fail: function (callback) {
                    callbacks.fail.push(callback);
                    if (status === -1) {
                        execute(callbacks.fail,args.reject);
                    }
                }
            };
        },

        resolve: function () {
            status = 1;
            args.resolve = arguments;
            execute(callbacks.done, args.resolve);
        },

        reject: function () {
            status = -1;
            args.reject = arguments;
            execute(callbacks.fail, args.reject);
        }
    };
}
\$\endgroup\$
  • \$\begingroup\$ These things are not "thenable" and they don't integrate with other "thenable" things, so I don't think it's accurate to call them promises. Looks interesting, though. Will do a proper review after I get some sleep if nobody beats me to it. \$\endgroup\$ – Dagg Jun 13 '14 at 9:33
  • \$\begingroup\$ Since you're inspired by jQuery's deferred objects, why not implement the always callback as well? \$\endgroup\$ – Johan Jun 13 '14 at 13:08
  • \$\begingroup\$ Adding always is good idea. I've missed this because always call not required in my project at now \$\endgroup\$ – ofstudio Jun 13 '14 at 16:13
5
\$\begingroup\$

A few things caught my eye:

  • You can call resolve and later call reject too - or the other way around, or call either one multiple times. It should only allow the first resolve/reject call to have an effect.
  • status might as well be a string (pending, resolved, rejected) which obviate the need for a comment explaining the integer values, and would improve readability
  • The if(f) check in execute should probably be a full if(typeof f === 'function') check; lots of values are truthy without being functions. You could also do this check in done/fail and simply ignore/complain about non-function arguments.
  • There's some duplication between done/fail and resolve/reject.

For that last one, if the status becomes a string, it's also relative easy use that as a key for organizing the callbacks and arguments.

function defer() {
  var currentState = 'pending',
      callbacks    = { resolved: [], rejected: [] },
      args         = []; // only need 1 args array

  function execute(state) {
    var cb;
    while(callbacks[state].length) {
      cb = callbacks[state].shift();
      if( typeof cb === 'function' ) cb.apply(this, args);
    }
  }

  // generic function factory for done/fail functions
  function hook(state) {
    return function (cb) {
      callbacks[state].push(cb);
      if(currentState === state) execute(state);
    }
  }

  // generic function factory for resolve/reject functions
  function complete(state) {
    return function () {
      if(currentState !== 'pending') return; 
      args = Array.prototype.slice.call(arguments, 0);
      currentState = state;
      execute(state);
    }
  }

  return {
    promise: function () {
      return { done: hook('resolved'), fail: hook('rejected') };
    },
    resolve: complete('resolved'),
    reject:  complete('rejected')
  };
}

You may want to add then, always and isPending (or just state) functions. And once you've resolved or rejected, you might as well get rid of the callbacks you're not going to ever use (i.e. if resolved, clear out the reject-callbacks,and vice-versa).

Lastly, if you want to emulate jQuery, make it chainable, so you can call x.done(...).fail(...).fail(...).always(...) etc.

\$\endgroup\$
  • \$\begingroup\$ Thank you for your valuable comments (especially about if check). Now the code looks much cleaner! \$\endgroup\$ – ofstudio Jun 13 '14 at 16:16
  • \$\begingroup\$ @ofstudio No problem. And (looking at the commments above), it'd be nice to add always and possibly then - and it should be pretty straightforward too. By the way, I'll just simplify my code slightly. \$\endgroup\$ – Flambino Jun 13 '14 at 16:26
0
\$\begingroup\$

I've added always handler. Now the code looks like this (thanx to Flambino)

function defer() {
    var currentState = 'pending',
        callbacks    = { resolved: [], rejected: [], always: [] },
        args         = []; //

    function execute(state) {
        var cb;
        while(callbacks[state].length) {
            cb = callbacks[state].shift();
            if( typeof cb === 'function' ) cb.apply(this, args);
        }
    }

    // generic function factory for done/fail functions
    function handle(state) {
        return function (cb) {
            callbacks[state].push(cb);
            if(currentState !== 'pending') {
                if(currentState === state) execute(state);
                if(state === 'always') execute('always');
            }
        }
    }

    // generic function factory for resolve/reject functions
    function complete(state) {
        return function () {
            if(currentState !== 'pending') return;
            args = Array.prototype.slice.call(arguments, 0);
            currentState = state;
            execute(state);
            execute('always');
        }
    }

    return {
        promise: function () {
            return {
                done: handle('resolved'),
                fail: handle('rejected'),
                always: handle('always')
            };
        },
        resolve: complete('resolved'),
        reject:  complete('rejected')
    };
}

Very useful thing (missed in initial version):
args = Array.prototype.slice.call(arguments, 0);
This line of code magically converts arguments from object into array

\$\endgroup\$
  • \$\begingroup\$ There is no reason to convert the arguments list to an array, since it is never exposed and is only passed as an argument to apply, which will happily take either an array or an instance of Arguments. \$\endgroup\$ – Dagg Jun 13 '14 at 23:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.