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);
 }
 };
}
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Jun 13, 2014 at 9:03
\$\endgroup\$
3
  • \$\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\$ Commented Jun 13, 2014 at 9:33
  • \$\begingroup\$ Since you're inspired by jQuery's deferred objects, why not implement the always callback as well? \$\endgroup\$ Commented Jun 13, 2014 at 13:08
  • \$\begingroup\$ Adding always is good idea. I've missed this because always call not required in my project at now \$\endgroup\$ Commented Jun 13, 2014 at 16:13

2 Answers 2

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.

answered Jun 13, 2014 at 12:34
\$\endgroup\$
2
  • \$\begingroup\$ Thank you for your valuable comments (especially about if check). Now the code looks much cleaner! \$\endgroup\$ Commented Jun 13, 2014 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\$ Commented Jun 13, 2014 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\$
1
  • \$\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\$ Commented Jun 13, 2014 at 23:26

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.