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);
}
};
}
2 Answers 2
A few things caught my eye:
- You can call
resolve
and later callreject
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 inexecute
should probably be a fullif(typeof f === 'function')
check; lots of values are truthy without being functions. You could also do this check indone
/fail
and simply ignore/complain about non-function arguments. - There's some duplication between
done
/fail
andresolve
/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.
-
\$\begingroup\$ Thank you for your valuable comments (especially about if check). Now the code looks much cleaner! \$\endgroup\$ofstudio– ofstudio2014年06月13日 16:16:33 +00:00Commented Jun 13, 2014 at 16:16
-
\$\begingroup\$ @ofstudio No problem. And (looking at the commments above), it'd be nice to add
always
and possiblythen
- and it should be pretty straightforward too. By the way, I'll just simplify my code slightly. \$\endgroup\$Flambino– Flambino2014年06月13日 16:26:07 +00:00Commented Jun 13, 2014 at 16:26
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
-
\$\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 ofArguments
. \$\endgroup\$Dagg– Dagg2014年06月13日 23:26:34 +00:00Commented Jun 13, 2014 at 23:26
always
callback as well? \$\endgroup\$always
is good idea. I've missed this becausealways
call not required in my project at now \$\endgroup\$