$.Callbacks, $.Topic and notify/progress on $.Deferred.

This commit is contained in:
jaubourg
2011-09-12 17:37:41 +02:00
committed by timmywil
parent 1878885fb7
commit 4092e3d275
22 changed files with 788 additions and 274 deletions

View File

@@ -43,7 +43,7 @@ var r20 = /%20/g,
// Document location segments
ajaxLocParts,
// Avoid comment-prolog char sequence (#10098); must appease lint and evade compression
allTypes = ["*/"] + ["*"];
@@ -403,7 +403,7 @@ jQuery.extend({
jQuery( callbackContext ) : jQuery.event,
// Deferreds
deferred = jQuery.Deferred(),
completeDeferred = jQuery._Deferred(),
completeDeferred = jQuery.Callbacks( "once memory" ),
// Status-dependent callbacks
statusCode = s.statusCode || {},
// ifModified key
@@ -582,7 +582,7 @@ jQuery.extend({
}
// Complete
completeDeferred.resolveWith( callbackContext, [ jqXHR, statusText ] );
completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
if ( fireGlobals ) {
globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] );
@@ -597,7 +597,7 @@ jQuery.extend({
deferred.promise( jqXHR );
jqXHR.success = jqXHR.done;
jqXHR.error = jqXHR.fail;
jqXHR.complete = completeDeferred.done;
jqXHR.complete = completeDeferred.add;
// Status-dependent callbacks
jqXHR.statusCode = function( map ) {

257
src/callbacks.js Normal file
View File

@@ -0,0 +1,257 @@
(function( jQuery ) {
// String to Object flags format cache
var flagsCache = {};
// Convert String-formatted flags into Object-formatted ones and store in cache
function createFlags( flags ) {
var object = flagsCache[ flags ] = {},
i, length;
flags = flags.split( /\s+/ );
for ( i = 0, length = flags.length; i < length; i++ ) {
object[ flags[i] ] = true;
}
return object;
}
/*
* Create a callback list using the following parameters:
*
* flags: an optional list of space-separated flags that will change how
* the callback list behaves
*
* filter: an optional function that will be applied to each added callbacks,
* what filter returns will then be added provided it is not falsy.
*
* By default a callback list will act like an event callback list and can be
* "fired" multiple times.
*
* Possible flags:
*
* once: will ensure the callback list can only be fired once (like a Deferred)
*
* memory: will keep track of previous values and will call any callback added
* after the list has been fired right away with the latest "memorized"
* values (like a Deferred)
*
* queue: only first callback in the list is called each time the list is fired
* (cannot be used in conjunction with memory)
*
* unique: will ensure a callback can only be added once (no duplicate in the list)
*
* relocate: like "unique" but will relocate the callback at the end of the list
*
* stopOnFalse: interrupt callings when a callback returns false
*
* addAfterFire: if callbacks are added while firing, then they are not executed until after
* the next call to fire/fireWith
*
*/
jQuery.Callbacks = function( flags, filter ) {
// flags are optional
if ( typeof flags !== "string" ) {
filter = flags;
flags = undefined;
}
// Convert flags from String-formatted to Object-formatted
// (we check in cache first)
flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {};
var // Actual callback list
list = [],
// Stack of fire calls for repeatable lists
stack = [],
// Last fire value (for non-forgettable lists)
memory,
// Flag to know if list is currently firing
firing,
// First callback to fire (used internally by add and fireWith)
firingStart,
// End of the loop when firing
firingLength,
// Index of currently firing callback (modified by remove if needed)
firingIndex,
// Add one or several callbacks to the list
add = function( args ) {
var i,
length,
elem,
type,
actual;
for ( i = 0, length = args.length; i < length; i++ ) {
elem = args[ i ];
type = jQuery.type( elem );
if ( type === "array" ) {
// Inspect recursively
add( elem );
} else if ( type === "function" ) {
// If we have to relocate, we remove the callback
// if it already exists
if ( flags.relocate ) {
self.remove( elem );
// Skip if we're in unique mode and callback is already in
} else if ( flags.unique && self.has( elem ) ) {
continue;
}
// Get the filtered function if needs be
actual = filter ? filter( elem ) : elem;
if ( actual ) {
list.push( [ elem, actual ] );
}
}
}
},
// Fire callbacks
fire = function( context, args ) {
args = args || [];
memory = !flags.memory || [ context, args ];
firing = true;
firingIndex = firingStart || 0;
firingStart = 0;
firingLength = list.length;
for ( ; list && firingIndex < firingLength; firingIndex++ ) {
if ( list[ firingIndex ][ 1 ].apply( context, args ) === false && flags.stopOnFalse ) {
memory = true; // Mark as halted
break;
} else if ( flags.queue ) {
list.splice( firingIndex, 1 );
break;
}
}
firing = false;
if ( list ) {
if ( !flags.once ) {
if ( stack && stack.length ) {
memory = stack.shift();
self.fireWith( memory[ 0 ], memory[ 1 ] );
}
} else if ( memory === true ) {
self.disable();
} else {
list = [];
}
}
},
// Actual Callbacks object
self = {
// Add a callback or a collection of callbacks to the list
add: function() {
if ( list ) {
var length = list.length;
add( arguments );
// Do we need to add the callbacks to the
// current firing batch?
if ( firing ) {
if ( !flags.addAfterFire ) {
firingLength = list.length;
}
// With memory, if we're not firing then
// we should call right away, unless previous
// firing was halted (stopOnFalse)
} else if ( memory && memory !== true ) {
firingStart = length;
fire( memory[ 0 ], memory[ 1 ] );
}
}
return this;
},
// Remove a callback from the list
remove: function() {
if ( list ) {
var args = arguments,
argIndex = 0,
argLength = args.length;
for ( ; argIndex < argLength ; argIndex++ ) {
for ( var i = 0; i < list.length; i++ ) {
if ( args[ argIndex ] === list[ i ][ 0 ] ) {
// Handle firingIndex and firingLength
if ( firing ) {
if ( i <= firingLength ) {
firingLength--;
if ( i <= firingIndex ) {
firingIndex--;
}
}
}
// Remove the element
list.splice( i--, 1 );
// If we have some unicity property then
// we only need to do this once
if ( flags.unique || flags.relocate ) {
break;
}
}
}
}
}
return this;
},
// Control if a given callback is in the list
has: function( fn ) {
if ( list ) {
var i = 0,
length = list.length;
for ( ; i < length; i++ ) {
if ( fn === list[ i ][ 0 ] ) {
return true;
}
}
}
return false;
},
// Remove all callbacks from the list
empty: function() {
list = [];
return this;
},
// Have the list do nothing anymore
disable: function() {
list = stack = memory = undefined;
return this;
},
// Is it disabled?
disabled: function() {
return !list;
},
// Lock the list in its current state
lock: function() {
stack = undefined;
if ( !memory || memory === true ) {
self.disable();
}
return this;
},
// Is it locked?
locked: function() {
return !stack;
},
// Call all callbacks with the given context and arguments
fireWith: function( context, args ) {
if ( stack ) {
if ( firing ) {
if ( !flags.once ) {
stack.push( [ context, args ] );
}
} else if ( !( flags.once && memory ) ) {
fire( context, args );
}
}
return this;
},
// Call all the callbacks with the given arguments
fire: function() {
self.fireWith( this, arguments );
return this;
},
// To know if the callbacks have already been called at least once
fired: function() {
return !!memory;
}
};
return self;
};
})( jQuery );

View File

@@ -258,7 +258,7 @@ jQuery.fn = jQuery.prototype = {
jQuery.bindReady();
// Add the callback
readyList.done( fn );
readyList.add( fn );
return this;
},
@@ -413,7 +413,7 @@ jQuery.extend({
}
// If there are functions bound, to execute
readyList.resolveWith( document, [ jQuery ] );
readyList.fireWith( document, [ jQuery ] );
// Trigger any bound ready events
if ( jQuery.fn.trigger ) {
@@ -427,7 +427,7 @@ jQuery.extend({
return;
}
readyList = jQuery._Deferred();
readyList = jQuery.Callbacks( "once memory" );
// Catch cases where $(document).ready() is called after the
// browser event has already occurred.

View File

@@ -1,187 +1,127 @@
(function( jQuery ) {
var // Promise methods
promiseMethods = "done fail isResolved isRejected promise then always pipe".split( " " ),
promiseMethods = "done removeDone fail removeFail progress removeProgress isResolved isRejected promise then always pipe".split( " " ),
// Static reference to slice
sliceDeferred = [].slice;
jQuery.extend({
// Create a simple deferred (one callbacks list)
_Deferred: function() {
var // callbacks list
callbacks = [],
// stored [ context , args ]
fired,
// to avoid firing when already doing so
firing,
// flag to know if the deferred has been cancelled
cancelled,
// the deferred itself
deferred = {
// done( f1, f2, ...)
done: function() {
if ( !cancelled ) {
var args = arguments,
i,
length,
elem,
type,
_fired;
if ( fired ) {
_fired = fired;
fired = 0;
}
for ( i = 0, length = args.length; i < length; i++ ) {
elem = args[ i ];
type = jQuery.type( elem );
if ( type === "array" ) {
deferred.done.apply( deferred, elem );
} else if ( type === "function" ) {
callbacks.push( elem );
Deferred: function( func ) {
var doneList = jQuery.Callbacks( "once memory" ),
failList = jQuery.Callbacks( "once memory" ),
progressList = jQuery.Callbacks( "memory" ),
promise,
deferred = {
// Copy existing methods from lists
done: doneList.add,
removeDone: doneList.remove,
fail: failList.add,
removeFail: failList.remove,
progress: progressList.add,
removeProgress: progressList.remove,
resolve: doneList.fire,
resolveWith: doneList.fireWith,
reject: failList.fire,
rejectWith: failList.fireWith,
notify: progressList.fire,
notifyWith: progressList.fireWith,
isResolved: doneList.fired,
isRejected: failList.fired,
// Create Deferred-specific methods
then: function( doneCallbacks, failCallbacks, progressCallbacks ) {
deferred.done( doneCallbacks ).fail( failCallbacks ).progress( progressCallbacks );
return this;
},
always: function() {
return deferred.done.apply( deferred, arguments ).fail.apply( this, arguments );
},
pipe: function( fnDone, fnFail, fnProgress ) {
return jQuery.Deferred(function( newDefer ) {
jQuery.each( {
done: [ fnDone, "resolve" ],
fail: [ fnFail, "reject" ],
progress: [ fnProgress, "notify" ]
}, function( handler, data ) {
var fn = data[ 0 ],
action = data[ 1 ],
returned;
if ( jQuery.isFunction( fn ) ) {
deferred[ handler ](function() {
returned = fn.apply( this, arguments );
if ( returned && jQuery.isFunction( returned.promise ) ) {
returned.promise().then( newDefer.resolve, newDefer.reject, newDefer.notify );
} else {
newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] );
}
});
} else {
deferred[ handler ]( newDefer[ action ] );
}
});
}).promise();
},
// Get a promise for this deferred
// If obj is provided, the promise aspect is added to the object
promise: function( obj ) {
if ( obj == null ) {
if ( promise ) {
return promise;
}
if ( _fired ) {
deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] );
}
promise = obj = {};
}
return this;
},
// resolve with given context and args
resolveWith: function( context, args ) {
if ( !cancelled && !fired && !firing ) {
// make sure args are available (#8421)
args = args || [];
firing = 1;
try {
while( callbacks[ 0 ] ) {
callbacks.shift().apply( context, args );
}
}
finally {
fired = [ context, args ];
firing = 0;
}
var i = promiseMethods.length;
while( i-- ) {
obj[ promiseMethods[i] ] = deferred[ promiseMethods[i] ];
}
return this;
},
// resolve with this as context and given arguments
resolve: function() {
deferred.resolveWith( this, arguments );
return this;
},
// Has this deferred been resolved?
isResolved: function() {
return !!( firing || fired );
},
// Cancel
cancel: function() {
cancelled = 1;
callbacks = [];
return this;
return obj;
}
};
return deferred;
},
// Handle lists exclusiveness
deferred.done( failList.disable, progressList.lock )
.fail( doneList.disable, progressList.lock );
// Full fledged deferred (two callbacks list)
Deferred: function( func ) {
var deferred = jQuery._Deferred(),
failDeferred = jQuery._Deferred(),
promise;
// Add errorDeferred methods, then and promise
jQuery.extend( deferred, {
then: function( doneCallbacks, failCallbacks ) {
deferred.done( doneCallbacks ).fail( failCallbacks );
return this;
},
always: function() {
return deferred.done.apply( deferred, arguments ).fail.apply( this, arguments );
},
fail: failDeferred.done,
rejectWith: failDeferred.resolveWith,
reject: failDeferred.resolve,
isRejected: failDeferred.isResolved,
pipe: function( fnDone, fnFail ) {
return jQuery.Deferred(function( newDefer ) {
jQuery.each( {
done: [ fnDone, "resolve" ],
fail: [ fnFail, "reject" ]
}, function( handler, data ) {
var fn = data[ 0 ],
action = data[ 1 ],
returned;
if ( jQuery.isFunction( fn ) ) {
deferred[ handler ](function() {
returned = fn.apply( this, arguments );
if ( returned && jQuery.isFunction( returned.promise ) ) {
returned.promise().then( newDefer.resolve, newDefer.reject );
} else {
newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] );
}
});
} else {
deferred[ handler ]( newDefer[ action ] );
}
});
}).promise();
},
// Get a promise for this deferred
// If obj is provided, the promise aspect is added to the object
promise: function( obj ) {
if ( obj == null ) {
if ( promise ) {
return promise;
}
promise = obj = {};
}
var i = promiseMethods.length;
while( i-- ) {
obj[ promiseMethods[i] ] = deferred[ promiseMethods[i] ];
}
return obj;
}
});
// Make sure only one callback list will be used
deferred.done( failDeferred.cancel ).fail( deferred.cancel );
// Unexpose cancel
delete deferred.cancel;
// Call given func if any
if ( func ) {
func.call( deferred, deferred );
}
// All done!
return deferred;
},
// Deferred helper
when: function( firstParam ) {
var args = arguments,
var args = sliceDeferred.call( arguments, 0 ),
i = 0,
length = args.length,
pValues = new Array( length ),
count = length,
pCount = length,
deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ?
firstParam :
jQuery.Deferred();
jQuery.Deferred(),
promise = deferred.promise();
function resolveFunc( i ) {
return function( value ) {
args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value;
if ( !( --count ) ) {
// Strange bug in FF4:
// Values changed onto the arguments object sometimes end up as undefined values
// outside the $.when method. Cloning the object into a fresh array solves the issue
deferred.resolveWith( deferred, sliceDeferred.call( args, 0 ) );
deferred.resolveWith( deferred, args );
}
};
}
function progressFunc( i ) {
return function( value ) {
pValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value;
deferred.notifyWith( promise, pValues );
};
}
if ( length > 1 ) {
for( ; i < length; i++ ) {
if ( args[ i ] && jQuery.isFunction( args[ i ].promise ) ) {
args[ i ].promise().then( resolveFunc(i), deferred.reject );
if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) {
args[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) );
} else {
--count;
}
@@ -192,8 +132,8 @@ jQuery.extend({
} else if ( deferred !== firstParam ) {
deferred.resolveWith( deferred, length ? [ firstParam ] : [] );
}
return deferred.promise();
return promise;
}
});
})( jQuery );
})( jQuery );

View File

@@ -14,7 +14,7 @@ function handleQueueMarkDefer( elem, type, src ) {
if ( !jQuery._data( elem, queueDataKey ) &&
!jQuery._data( elem, markDataKey ) ) {
jQuery.removeData( elem, deferDataKey, true );
defer.resolve();
defer.fire();
}
}, 0 );
}
@@ -160,9 +160,9 @@ jQuery.fn.extend({
if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) ||
( jQuery.data( elements[ i ], queueDataKey, undefined, true ) ||
jQuery.data( elements[ i ], markDataKey, undefined, true ) ) &&
jQuery.data( elements[ i ], deferDataKey, jQuery._Deferred(), true ) )) {
jQuery.data( elements[ i ], deferDataKey, jQuery.Callbacks( "once memory" ), true ) )) {
count++;
tmp.done( resolve );
tmp.add( resolve );
}
}
resolve();

45
src/topic.js Normal file
View File

@@ -0,0 +1,45 @@
(function( jQuery ) {
var topics = {},
sliceTopic = [].slice;
jQuery.Topic = function( id ) {
var callbacks,
method,
topic = id && topics[ id ];
if ( !topic ) {
callbacks = jQuery.Callbacks();
topic = {
publish: callbacks.fire,
subscribe: callbacks.add,
unsubscribe: callbacks.remove
};
if ( id ) {
topics[ id ] = topic;
}
}
return topic;
};
jQuery.extend({
subscribe: function( id ) {
var topic = jQuery.Topic( id ),
args = sliceTopic.call( arguments, 1 );
topic.subscribe.apply( topic, args );
return {
topic: topic,
args: args
};
},
unsubscribe: function( id ) {
var topic = id && id.topic || jQuery.Topic( id );
topic.unsubscribe.apply( topic, id && id.args ||
sliceTopic.call( arguments, 1 ) );
},
publish: function( id ) {
var topic = jQuery.Topic( id );
topic.publish.apply( topic, sliceTopic.call( arguments, 1 ) );
}
});
})( jQuery );