The Deferred module is not a core requirement, but it is essential for implementing promise-based functionality in the ajax module. The Deferred module also utilzies the Callbacks module, which was covered in a previous article.
Source Code Version
This article references the source code from Zepto version 1.2.0.
Promise/A+ Specification
The Promise/A+ specification defines the behavior of promises. A promise can be in one of three states: pending, fulfilled, or rejected. A promise must have a then method that accepts two arguments: an onFulfilled functon and an onRejected function.
Structure of the Deferred Module
(function($) {
function Deferred(func) {
var deferred = {};
if (func) func.call(deferred, deferred);
return deferred;
}
$.Deferred = Deferred;
})(Zepto);
The Deferred function returns an object that conforms to the Promise/A+ specification. If a function is passed as an argument, it is executed with the deferred object as both the context and the argument.
Generating Methods
var tuples = [
["resolve", "done", $.Callbacks({once: 1, memory: 1}), "resolved"],
["reject", "fail", $.Callbacks({once: 1, memory: 1}), "rejected"],
["notify", "progress", $.Callbacks({memory: 1})]
],
state = "pending",
promise = {},
deferred = {};
$.each(tuples, function(i, tuple) {
var list = tuple[2],
stateString = tuple[3];
promise[tuple[1]] = list.add;
if (stateString) {
list.add(function() {
state = stateString;
}, tuples[i ^ 1][2].disable, tuples[2][2].lock);
}
deferred[tuple[0]] = function() {
deferred[tuple[0] + "With"](this === deferred ? promise : this, arguments);
return this;
};
deferred[tuple[0] + "With"] = list.fireWith;
});
Explanation of Variables
- tuples: Stores method names, corresponding methods, callback lists, and final state strings.
- state: Current state of the promise.
- promise: Contains methods like always, then, done, fail, progress, and helper methods like state and promise.
- deferred: In addition to inheriting promise methods, it includes methods like resolve, resolveWith, reject, rejectWith, notify, and notifyWith.
Generating done, fail, and progress Methods
The methods are geneerated by iterating over the tuples array:
var list = tuple[2],
stateString = tuple[3];
promise[tuple[1]] = list.add;
list is created using the $.Callbacks factory method. The memory: 1 option ensures that added functions are immediately triggered. The once: 1 option ensures that the callbacks can only be fired once.
State Transition
if (stateString) {
list.add(function() {
state = stateString;
}, tuples[i ^ 1][2].disable, tuples[2][2].lock);
}
If stateString exists, a function is added to the callback list to update the state. The ^ operator is used to disable the opposite state's callback list, ensuring mutual exclusivity.
Generating resolve/resolveWith, reject/rejectWith, and notify/notifyWith Methods
deferred[tuple[0] + "With"] = list.fireWith;
deferred[tuple[0]] = function() {
deferred[tuple[0] + "With"](this === deferred ? promise : this, arguments);
return this;
};
These methods are stored in the deferred object. The resolveWith, rejectWith, and notifyWith methods are equivalent to the fireWith method of $.Callbacks.
Promise Object Methods
.state()
state: function() {
return state;
},
Returns the current state of the promise.
.always()
always: function() {
deferred.done(arguments).fail(arguments);
return this;
},
Executes the provided callbacks regardless of the promise's state.
.promise()
promise: function(obj) {
return obj != null ? $.extend(obj, promise) : promise;
},
Returns the promise object, extending it with the methods of the promise object if an obj is provided.
.then()
then: function(/* fnDone [, fnFailed [, fnProgress]] */) {
var fns = arguments;
return Deferred(function(defer) {
$.each(tuples, function(i, tuple) {
var fn = $.isFunction(fns[i]) && fns[i];
deferred[tuple[1]](function() {
var returned = fn && fn.apply(this, arguments);
if (returned && $.isFunction(returned.promise)) {
returned.promise()
.done(defer.resolve)
.fail(defer.reject)
.progress(defer.notify);
} else {
var context = this === promise ? defer.promise() : this,
values = fn ? [returned] : arguments;
defer[tuple[0] + "With"](context, values);
}
});
});
fns = null;
}).promise();
},
The then method accepts up to three callback functions and returns a new promise.
$.when
$.when = function(sub) {
var resolveValues = Array.prototype.slice.call(arguments),
len = resolveValues.length,
i = 0,
remain = len !== 1 || (sub && $.isFunction(sub.promise)) ? len : 0,
deferred = remain === 1 ? sub : Deferred(),
progressValues, progressContexts, resolveContexts,
updateFn = function(i, ctx, val) {
return function(value) {
ctx[i] = this;
val[i] = arguments.length > 1 ? Array.prototype.slice.call(arguments) : value;
if (val === progressValues) {
deferred.notifyWith(ctx, val);
} else if (!(--remain)) {
deferred.resolveWith(ctx, val);
}
};
};
if (len > 1) {
progressValues = new Array(len);
progressContexts = new Array(len);
resolveContexts = new Array(len);
for (; i < len; ++i) {
if (resolveValues[i] && $.isFunction(resolveValues[i].promise)) {
resolveValues[i].promise()
.done(updateFn(i, resolveContexts, resolveValues))
.fail(deferred.reject)
.progress(updateFn(i, progressContexts, progressValues));
} else {
--remain;
}
}
}
if (!remain) deferred.resolveWith(resolveContexts, resolveValues);
return deferred.promise();
};
The $.when method manages multiple asynchronous operations. It resolves when all operations succeed and rejects if any operation fails.
Variables
- resolveValues: Array of all asynchronous objects.
- len: Number of asynchronous objects.
- remain: Number of remaining asynchronous operations.
- i: Index of the current asynchronous operation.
- deferred: The deferred object.
- progressValues, progressContexts, resolveContexts: Arrays for managing progress and resolution contexts.
updateFn
updateFn = function(i, ctx, val) {
return function(value) {
ctx[i] = this;
val[i] = arguments.length > 1 ? Array.prototype.slice.call(arguments) : value;
if (val === progressValues) {
deferred.notifyWith(ctx, val);
} else if (!(--remain)) {
deferred.resolveWith(ctx, val);
}
};
};
updateFn is called when each asynchronous operation is resolved or progresses.
Processing Asynchronous Operations
if (len > 1) {
progressValues = new Array(len);
progressContexts = new Array(len);
resolveContexts = new Array(len);
for (; i < len; ++i) {
if (resolveValues[i] && $.isFunction(resolveValues[i].promise)) {
resolveValues[i].promise()
.done(updateFn(i, resolveContexts, resolveValues))
.fail(deferred.reject)
.progress(updateFn(i, progressContexts, progressValues));
} else {
--remain;
}
}
}
if (!remain) deferred.resolveWith(resolveContexts, resolveValues);
return deferred.promise();
This code initializes the necessary arrays and processes each asynchronous operation, updating the state and resolving the promise when all operations complete.