📜 ⬆️ ⬇️

What is this new jQuery.Callbacks Object

In the not so long ago released version of jQuery 1.7, a new Callbacks object appeared, which will be discussed today.
The official documentation of jQuery.Callbacks is described as a multipurpose object, which is a list of callback functions (callbacks - hereinafter simply callbacks) and powerful tools for managing this list.

I looked at the capabilities of this object when it was still only in development, and I must say that it initially had a bit more capabilities than it was left in the release version. For example, now there is no possibility to create queue (callback) callbacks, which are called one for each fire() call. Apparently, the jQuery team decided to cut the code a bit by removing the “unnecessary / rarely used” features to save on the weight of the library. This is a small excursion into the history of Callbacks, but then I will describe only the functions currently available and at the end I will write a small possible improvement to this object.

Purpose


Before starting a detailed study of this new jQuery.Callbacks object, I want to dwell on what this object is for. Quite often in JavaScript code callbacks are used - functions that are called upon the occurrence of some event, for example, after the completion of some action, the most vivid example is the AJAX request. And at the same time, there is often a need to call not one function, but several at once (how many, maybe a couple, maybe a couple of dozen, or maybe not even a single one) is unknown - this is the well-known and simple “Observer” pattern . And for such cases, the jQuery.Callbacks object in question turns out to be useful. In jQuery itself, this object is used (since version 1.7) inside jQuery.Deferred and jQuery.ajax. Also, the jQuery authors made this object publicly available and documented it so that other developers could use it when implementing their own components.
')

Constructor: jQuery.Callbacks (flags)


By calling the constructor, a callbacks object is created, which has a number of methods for managing the list of callbacks.
The flags parameter is optional and allows you to set the parameters of the object, the possible values ​​of the parameter we consider below.
 var callbacks = $.Callbacks(); 

To the created callbacks object, we can now add callbacks functions to the list, delete them, call, call again (if this was not forbidden when creating the object), check the status of the object (whether there was already a call or not) using such object methods as add() , remove() , fire() , etc. Callbacks are executed, by the way, in the order they are added to the list.

I note that this is not a “real” class instance constructor; therefore, using the new operator when calling it is not required (and even meaningless).

For this reason, it will not be possible to check whether the object is an instance of Callbacks, in a manner standard for JS:
 if (obj instanceof $.Callbacks) { obj.add(fn); } 

An if statement always returns false. But you can rely on one of the known methods of this object (or several at once), for example, you can check this:
 if (obj.fire) { obj.add(fn); } 


Actually, inside this function, a regular JS object is created with a certain set of methods that rely on its closure - this is a fairly common way in JavaScript to set private variables that are inaccessible outside of this pseudo-constructor.

Also, thanks to such a pseudo-constructor, the methods of this object do not depend on the context of the call — the object to which they belong, which means that they can be safely assigned to properties of another object without worrying about changing the context — everything will still work correctly. This is true for all methods except fire , it just depends on the context, but uses it as the execution context for callbacks from the list, i.e. in some cases this method is not just possible, but it is necessary to assign the properties of another object with a change of context. For example:
 var c = $.Callbacks(), obj = {}; obj.register = c.add; obj.register(function() { console.log('fired'); }); c.fire(); // output: 'fired' 


Flags


Note : hereinafter, the words "call the fire() method" means calling the callbacks from the list including the fireWith() method.

The parameter of the flags constructor is a string in which you can specify flags — options — in accordance with which the created callbacks object will work. The following flags are supported:

once indicates that the list of callbacks can be executed only once, the second and subsequent calls to the fire() method will fail (as was done in the deferred object); if this flag is not specified, you can call the fire() method several times.

memory indicates that it is necessary to memorize the parameters of the last call to the fire() method (and make callbacks from the list) and immediately add the added callbacks with the appropriate parameters if they are added after calling the fire() method (as was done in the deferred object).

unique - indicates that each callback can only be added to the list once, a second attempt to add a callback to the list will lead to nothing.

stopOnFalse indicates that it is necessary to stop the execution of callbacks from the list, if any of them returned false , during the current session, call fire() . The next call to the fire() method starts a new session of executing the list of callbacks, and they will be executed again until one of the list returns false or ends.

Methods


Below I will give a list of methods with a brief description, examples are in the official docks and for some of the methods in the next section. In general, the methods are quite simple and behave quite expectedly.

callbacks.add (callbacks) returns: callbacks - adds callbacks to the list, you can simultaneously pass several functions (several arguments) or function arrays (you can simultaneously do both) in the arguments of this method, you can even pass nested arrays. All arguments (or array elements) that are not functions are simply ignored. The method (this and some further) returns the context of its call, thereby allowing to organize a chain of calls to several methods of one object in a row, as is customary in jQuery.

callbacks.remove (callbacks) returns: callbacks - removes callbacks from the list, and even if the callback was added twice, it will be deleted from both positions. So If you call the method of removing some callback from the list, then you can be sure that it is no longer on the list, no matter how many times it is added. You can pass several functions at the same time as several arguments, you cannot pass arrays, all arguments of non-functions are ignored.

callbacks.has (callback) returns: boolean - checks if the specified function is in the callback list.

callbacks.empty () returns: callbacks - clears the list of callbacks.

callbacks.disable () returns: callbacks - “disables” the callbacks object, all actions with it will be ineffective. In this case, all methods cease to work at all: add - does not lead to anything, has - always returns false , etc.

callbacks.disabled () returns: boolean - checks if the callbacks object is disabled, after a call, disable() will return true .

callbacks.lock () returns: callbacks - fixes the current state of the callbacks object with respect to the parameters and execution status of the callback list. This method is relevant when using the memory flag and is designed to block only subsequent fire() calls, otherwise it is equivalent to the disable() call.
This method works in detail as follows: if the memory flag is not specified or the fire() method has never been called or the last session of callbacks was interrupted by one of them returning false , then the lock() call is equivalent to the disable() call (this is what inside) and calling disabled() in this case returns true , otherwise only subsequent fire() calls will be blocked - they will neither lead to callbacks, nor change the execution parameters of added callbacks (if the memory flag is present).


callbacks.locked () returns: boolean - checks if the callbacks object is locked by the lock() method, also returns true after a call to disable() .

callbacks.fireWith ([context] [, args]) returns: callbacks - starts the execution of all callbacks in the list with the specified context and arguments. context - indicates the execution context of the callback (an object accessible via this inside the function). args is an array (just an array) of arguments passed to the callback.

callbacks.fire (arguments) returns: callbacks - starts the execution of all callbacks in the list with the context of the call and the arguments of this method. arguments is a list of arguments (not an array, as in the fireWith() method). Those. The call context and callback arguments will be the context and arguments of the fire() method.

An example of how you can equivalently start the execution of callbacks with the same parameters and context:
 var callbacks = $.Callbacks(), context = { test: 1 }; callbacks.add(function(p, t) { console.log(this.test, p, t); }); callbacks.fireWith(context, [ 2, 3 ]); // output: 1 2 3 context.fire = callbacks.fire; context.fire(2, 3); // output: 1 2 3 


Callbacks from the list are performed in the order in which they were added to this list. After the callbacks are executed with the specified flag once, the list will be cleared, and if the memory flag is not specified or the callbacks were interrupted by returning false , then the callbacks object will be disabled by the disable() method.

Examples


Let's see how the flags work with examples. All examples use the following functions:
 function fn1( value ){ console.log( value ); } function fn2( value ){ fn1("fn2 says:" + value); return false; } 


$ .Callbacks ():

 var callbacks = $.Callbacks(); callbacks.add( fn1 ); callbacks.fire( "foo" ); callbacks.add( fn2 ); callbacks.add( fn1 ); callbacks.fire( "bar" ); callbacks.remove( fn2 ); callbacks.fire( "foobar" ); console.log(callbacks.disabled()); /* output: foo bar fn2 says:bar bar foobar foobar false */ 

No flags indicated - quite expected behavior.

$ .Callbacks ('once'):

 var callbacks = $.Callbacks( "once" ); callbacks.add( fn1 ); callbacks.fire( "foo" ); callbacks.add( fn2 ); callbacks.add( fn1 ); callbacks.fire( "bar" ); callbacks.remove( fn2 ); callbacks.fire( "foobar" ); console.log(callbacks.disabled()); /* output: foo true */ 

Everything is clear here - once they did what happened, then nothing happens that they did, because the list is already disabled.

$ .Callbacks ('memory'):

 var callbacks = $.Callbacks( "memory" ); callbacks.add( fn1 ); callbacks.fire( "foo" ); callbacks.add( fn2 ); callbacks.add( fn1 ); callbacks.fire( "bar" ); callbacks.remove( fn2 ); callbacks.fire( "foobar" ); console.log(callbacks.disabled()); /* output: foo fn2 says:foo foo bar fn2 says:bar bar foobar foobar false */ 

Here, it seems, is also nothing complicated - after the first execution, each addition of a callback causes its immediate execution, and then again we trigger the execution of the entire list. At the same time, we added one function to the list twice - it works twice.

$ .Callbacks ('unique'):

 var callbacks = $.Callbacks( "unique" ); callbacks.add( fn1 ); callbacks.fire( "foo" ); callbacks.add( fn2 ); callbacks.add( fn1 ); callbacks.fire( "bar" ); callbacks.remove( fn2 ); callbacks.fire( "foobar" ); console.log(callbacks.disabled()); /* output: foo bar fn2 says:bar foobar false */ 

And in this case, the re-addition of the fn1 function was ignored.

$ .Callbacks ('stopOnFalse'):

 var callbacks = $.Callbacks( "stopOnFalse" ); callbacks.add( fn1 ); callbacks.fire( "foo" ); callbacks.add( fn2 ); callbacks.add( fn1 ); callbacks.fire( "bar" ); callbacks.remove( fn2 ); callbacks.fire( "foobar" ); console.log(callbacks.disabled()); /* output: foo bar fn2 says:bar foobar foobar false */ 

fn2 interrupts the execution chain, because returns false .

These are simple examples, and now let's try to play around with combinations of flags - it will be a little more interesting:

$ .Callbacks ('once memory'):

 var callbacks = $.Callbacks( "once memory" ); callbacks.add( fn1 ); callbacks.fire( "foo" ); callbacks.add( fn2 ); callbacks.add( fn1 ); callbacks.fire( "bar" ); callbacks.remove( fn2 ); callbacks.fire( "foobar" ); console.log(callbacks.disabled()); /* output: foo fn2 says:foo foo false */ 

We see that only the first fire() worked and the addition of new callbacks led to their immediate execution with the parameters of the first fire() .

$ .Callbacks ('once memory unique'):

 var callbacks = $.Callbacks( "once memory unique" ); callbacks.add( fn1 ); callbacks.fire( "foo" ); callbacks.add( fn2 ); callbacks.add( fn1 ); callbacks.fire( "bar" ); callbacks.remove( fn2 ); callbacks.fire( "foobar" ); console.log(callbacks.disabled()); /* output: foo fn2 says:foo foo false */ 

Here, the result is the same, despite the fact that we specified the unique flag and add fn1 twice, the second time adding this function to the list worked, because with the specified flag once after the callbacks are executed, the list is cleared, and the memory flag indicates that subsequent additions of callbacks will lead to their immediate execution without being placed on the list, and since the list is empty, the addition of any function is always unique. But this flag will play its role when you try to add several callbacks at once, among which there are duplicate ones, if in the previous code you change the 4th line as shown below, then fn2 still be executed only once (and without the unique flag, three times):
 callbacks.add( fn2, fn2, fn2 ); 


$ .Callbacks ('once memory stopOnFalse'):

 var callbacks = $.Callbacks( "once memory stopOnFalse" ); callbacks.add( fn1 ); callbacks.fire( "foo" ); callbacks.add( fn2 ); callbacks.add( fn1 ); callbacks.fire( "bar" ); callbacks.remove( fn2 ); callbacks.fire( "foobar" ); console.log(callbacks.disabled()); /* output: foo fn2 says:foo true */ 

Returning false blocked all further callbacks, and if there was a flag once, the callbacks object was turned off altogether.

I will not consider all possible combinations of flags, I tried to choose the most interesting (not quite simple) and explain the behavior of callbacks. The remaining combinations can be tested independently, for example, using the stock: http://jsfiddle.net/zandroid/JXqzB/

Promised improvement


Improvement, of course, is not at all mandatory, and even, perhaps, to some degree contrived, do not judge strictly.
The idea behind the improvement is to omit the call to the fire() method, and instead use the callbacks object itself as a function. To do this, we write the following function:
 (function($, undefined){ $.FCallbacks = function(flags, fns) { var i = $.type(flags) === 'string' ? 1 : 0, callbacks = $.Callbacks(i ? flags : undefined); callbacks.add(Array.prototype.slice.call(arguments, i)) return $.extend(callbacks.fire, callbacks, { fcallbacks: true }); }; })(jQuery); 

And without further ado let's see an example of use:
 function fn1(p1, p2) { console.log('fn1 says:', this, p1, p2); } function fn2(p1, p2) { console.log('fn2 says:', this, p1, p2); } var callbacks = $.FCallbacks('once', fn1, rn2); callbacks.add(fn2); callbacks(2, 3); 

Also, the new “constructor” has the opportunity to immediately pass the initial callbacks in the parameters, without unnecessary calling add() .
Well, in work: jsfiddle.net/zandroid/RAVtF

All with the coming holidays, thank you for your attention.

UPD:
Judging by the comments, I still in vain omitted information about how this object is used inside jQuery. Comments about “they did Deferred - and this is a double of such and such a method in such a framework” or “why this Callbacks are needed - it only weighs the weight of the jQuery library, but I don’t come up with real-world applications” - these are, in my opinion, not understanding the essence of the issue. Below, I want to explain this moment.

Real use


Callbacks are in fact now used by so many jQuery 1.7+ users and it was not easy for the development team because they wanted to make a new feature. See, the chain and logic of this question is quite simple:

The library implemented the $.ajax() method, which by its nature is nothing but an add-on for a certain Deferred - the developers improved the code, removed it separately from the $.ajax() main code (to be able to reuse and simplify testing) and decided, why not publish this code (give access to library users to it and document it) - it turned out $.Deferred .

In turn, $.Deferred is initially two ( done() and fail() ), and now three (+ more progress() ) add-ons over Callbacks, which was made as internal code $.Deferred . And again, the developers improved and separated this code from $.Deferred , implementing the latter through $.Callbacks (by the way, the $.Deferred source code became much clearer and more readable).

Conclusion: the developers do not set as the main goal to add new “useless” features, they optimize the already existing internal code, simultaneously publishing side effects that are not less useful for it. And every time you use $.ajax() , know that you are using $.Deferred , which means $.Callbacks . This is an example of real use.

Source: https://habr.com/ru/post/135821/


All Articles