Home  >  Article  >  Web Front-end  >  Detailed explanation of Callbacks in jQuery source code analysis_jquery

Detailed explanation of Callbacks in jQuery source code analysis_jquery

WBOY
WBOYOriginal
2016-05-16 16:09:471295browse

The essence of code highlights the concept of sequence and order, especially in JavaScript - after all, JavaScript is a single-threaded engine.

Javascript has the characteristics of functional programming, and because of the JavaScript single-threaded engine, our functions always need to be executed in an orderly manner. Excellent code often cuts functions into their own modules and then executes them under certain conditions. Since these functions are executed in an orderly manner, why don't we write a unified management object to help us manage these functions - so, Callbacks (callback functions) were born.

What are Callbacks

Javascript is full of functional programming. For example, the simplest window.onload accepts a function. The sad thing is that window.onload can only receive one function if it is assigned directly. If there are several functions that you want to execute in onload, Then we need to write the following code:

Copy code The code is as follows:

function a(elem) {
                elem.innerHTML = 'I am function a, I want to change the HTML structure of Element';
        };
         function b(elem) {
                elem.innerHTML = 'My function b, I want to change the style of Element';
}
          window.onload = function () {
               var elem = document.getElementById('test');
a(elem);
              b(elem);
        };

The original intention of the callback function is to build on such a thing. It no longer allows us to scatter these functions, but to organize these functions in a unified manner. As you can see, we want to do two things for an Element in window.onload: first change the html structure, and then change the style of the html. The two functions also operate on an Element, and the final execution of these two functions is performed in order. So why don't we write an object like this to manage these functions. Of course, this is only the most basic meaning of the callback function. We need more than just such a simple callback function object, we need a more powerful callback function. Well, this is just a simple use case, so I can tell you what this callback function can do besides executing functions one by one.

The essence of Callbacks is to control the orderly execution of functions. Javascript is a single-threaded engine, which means that only one code in JavaScript is running at the same time - even Ajax and setTimeout. These two functions seem to be asynchronous, but this is not the case. When the browser runs javascript code, these codes will be pushed into a queue in an orderly manner. When you run Ajax, the browser will Pushing into the code queue, the browser takes the code one by one from the code queue when processing the JavaScript code - Callbacks, catering to this single-threaded engine.

Of course, what we want is more than just such a simple tool object - in the jQuery source code, Callbacks provides the basic management of a set of functions, provides the foundation for Deferred (asynchronous queue), and also serves Queue (sync queue). Deferred is used to smooth/flatten pyramid programming (a large number of nested callback functions, such as code in Ajax that needs to be executed based on the request return code); and Queue drives jQuery.animate (animation engine).

Then let’s write a Callbacks.

Callbacks model

Array:
Since our Callbacks want to accept a series of functions, we must have a container. We can use an array and push each function into the array. When it needs to be executed, loop the array items to execute.

Working Model:

This Callbacks need to be very powerful. It is not just as simple as pushing a function and then executing it. This Callbacks should have a good execution model.
once: All functions in the current Callbacks object will only be executed once, and will be released after execution. We can provide a stable and effective solution for users who use the Callbacks object to ensure that the function will only be executed once and will not be executed again. , which stabilizes the threads of these functions.
auto: automatic execution model. This is an interesting model. Some functions depend on the previous layer function. For example, the execution of function b depends on function a. Then we provide an automatic execution model: after the first execution of this Callbacks, each time it is added When the function reaches the Callbacks, it automatically executes the functions added in the past and passes the last parameter data given to the past functions. This eliminates the need for repeated triggering between these dependent functions from the Callbacks. This It's an interesting model.
once&auto: We can make it more powerful and work with the once and auto models at the same time, that is: every time a function is added to Callbacks, the past functions will be executed, and then, these past functions will be released, and functions will continue to be added next time At that time, those functions in the past will no longer be executed because the once model has released them.

API:

add(function) - Add one (or more) functions to the Callbacks object: Of course, if you don’t add functions and are just curious to take a look at Callbacks, we will let you continue to enjoy your fun - we don’t will throw an exception because that's not something we're good at.
remove(function) - removes a function in a Callbacks: Now that we have added it, we should also provide a plan to regret it. We are so approachable and tolerate everything that others have done in the past.
has(function) - Determines whether Callbacks contain a function: Oh? You are not sure whether to include this function, but you threw it in in the first place! Why are you so careless? But since you asked me, I will still tell you whether Callbacks contains this function. I know you are very busy and cannot remember and determine everything.
empty() - Empty Callbacks: Have these functions lost their meaning to you? What? You don’t want it anymore after it’s been executed? So you wish you could clear it? Well, for the sake of memory, I still tolerate your demand.
disable() - Disable a Callbacks: In order to maintain a stable existence with other people's code, I chose self-sacrifice - yes, this method can disable Callbacks, completely disable it, as if it had not existed before.
disabled() - Determines whether the Callbacks have been disabled: If you still don't believe whether the Callbacks are really self-sacrificing, this method can give you peace of mind.
lock(boolean) - Lock this Callbacks object: you are afraid that it is unstable, but you don’t want to abandon it. Lock is a good method. It receives a Boolean parameter to indicate whether the object needs to be locked. Of course, it has no parameters. Allows you to determine whether Callbacks are locked.
fire(data) - Execute the function in this Callbacks: Isn't everything we do for the fate of execution at this moment? The parameters will become the parameters of these functions that need to be executed.
fireWith(context,data) - Execute the function in Callbacks and specify the context. In fire(), the Context of all functions are Callbacks objects, and fireWidth() allows you to redefine the context of these functions to be executed. How free programming is, Callbacks considers everything for you.
fired() - Determine whether this Callbacks has been executed in the past: We believe that most of the time you don't know what you have done in the past, but we record everything you do. If you have executed this Callbacks object in the past, then you can never deny it. , because we know whether you have executed this Callbacks in the past.

Basic module implementation

Simple implementation:
Let’s first simply implement a Callbacks:

Copy code The code is as follows:

(function (window, undefined) {
            var Callbacks = function () {
//Protect these private variables through closures
                   var list = [],//Callback function list
Fired; // Have you executed
//Return a closure Callbakcs object
                   return {
                         add: function (fn) {
//When Callbacks are discarded, the list is undefined
If (list) {
//Add a callback function
                                 list.push(fn);
//Support chain callbacks
                                                                                                       }                                                                                          return this;                     },
fireWith: function (context, data) {
//Trigger the callback function and specify the context
If (list) {
fired = true;
for (var i = 0, len = list.length; i < len; i ) {
//When a function in Callbacks returns false, stop the subsequent execution of Callbacks
If (list[i].apply(context, data) === false)
break;
                                                                                                   }                                                                                                        }                                                                                          return this;                     },
Fire: function () {
//Trigger callback function
//Call fireWith and specify the context
Return this.fireWith(this, arguments);
                    },
                      empty: function () {
//Clear the list
                                                                                                                                                   if (list)//When this Callbacks is discarded, Callbacks should not be able to continue to be used
list = [];
                                                                                         return this;                     },
                           disable: function () {
//Abandon this Callbacks object, and the subsequent callback function list will no longer be executed
                         list = undefined;
                                                                                         return this;                     },
                       disabled: function () {//Detect whether this Callbacks has been disabled
//Convert to boolean and return
Return !list;
                    },
                     fired: function () {//Whether this callbacks have been executed
                                                                                                                                                                                                                                     }
                };
            };
//Register to window
               window.Callbacks = Callbacks;
         }(window));



Then let’s test this Callbacks:

Copy code The code is as follows:

        var test = new Callbacks();
         test.add(function (value) {
                console.log('Function 1, value is: ' value);
        });
         test.add(function (value) {
​​​​​​ console.log('Function 2, value is:' value);
        });
​​​​ test.fire('This is the value of function 1 and function 2');
console.log('Check whether the function has been executed:' test.fired());
         test.disable();//Abandon this Callbacks
console.log('Check whether the function is abandoned:' test.disabled());
         test.add(function () {
console.log('Add a third function, this function should not be executed');
        });
         test.fire();

Open the browser console and we can see that the running results are normal.

once and auto(memory) implementation

once:
Once lets the function in this callback run once and then not run again. The principle is very simple. In the above code, we can see that there is a variable list that takes over the function list, so we only need to clear the codes that have been executed in the past. We use a global variable to save the current execution model. If it is a once model, just invalidate the list in fireWith():

Copy code The code is as follows:

(function (window, undefined) {
            var Callbacks = function (once) {
//Protect these private variables through closures
                   var list = [],//Callback function list
Fired; // Have you executed
//Return a closure Callbakcs object
                   return {
//...Omit some code
fireWith: function (context, data) {
//Trigger the callback function and specify the context
If (list) {
fired = true;
for (var i = 0, len = list.length; i < len; i ) {
//When a function in Callbacks returns false, stop the subsequent execution of Callbacks
If (list[i].apply(context, data) === false)
break;
                                                                                                   }                                                                                                        } //If the once model is configured, the global variable once is true, and the list is reset
If (once) list = undefined;
                                                                                         return this;                  }
//...Omit some code
                };
            };
//Register to window
               window.Callbacks = Callbacks;
         }(window));

auto:

The auto (memory) model is named after memory in jQuery. I was confused by this name at first. After carefully looking at the usage, I decided to change it to auto - its function is "after the first fire(), subsequent "add() function automatically executes" can be used in the following situations: after adding a set of functions to Callbacks, and temporarily need to add a function, then run the newly added function immediately - it must be said that for the convenience of use, This pattern becomes a bit difficult to understand. The implementation is to determine whether it is an auto model during add(). If it is an auto model, execute this function. However, we need to automatically execute it after the first fire(). Callbacks that have not been fired() should not be automatically executed. Moreover, after each automatic execution, the last used parameters need to be passed to this Automatically executed functions.

Perhaps you will think of the following code:

Copy code The code is as follows:

(function (window, undefined) {
            var Callbacks = function (once, auto) {
              var list = [],
fired,
lastData;//Save the parameters of the last execution
                   return {
                         add: function (fn) {
If (list) {
                                 list.push(fn);
​​​​​​​​​​​​​ //The last parameter used is passed, and the Context is lost here
//In order to prevent the context from being lost here, we may also need to declare a variable to save the last used Context
If (auto) this.fire(lastData);
                                                                                                       }                                                                                          return this;                     },
fireWith: function (context, data) {
If (list) {
lastData = data;// — Record the last used parameter
fired = true;
for (var i = 0, len = list.length; i < len; i ) {
If (list[i].apply(context, data) === false)
break;
                                                                                                   }                                                                                                        } If (once) list = [];
                                                                                         return this;                  }
//Part of the code is omitted
                };
            };
//Register to window
               window.Callbacks = Callbacks;
         }(window));

But a more wonderful usage is adopted in jQuery. The author of jQuery is also proud of this usage, so he named this model memory-that is, the above variable auto not only represents the current auto execution mode, but also serves as the last Container of parameters, which represents both auto and memory. (The following code is not jQuery and is written based on jQuery code ideas, not source code):

Copy code The code is as follows:

(function (window, undefined) {
            var Callbacks = function (auto) {
              var list = [],
fired,
Memory,//The main actor is here, it’s memory
coreFire = function (data) {
//The real trigger function method
If (list) {
                                                                                                                                                   Memory = auto && data;//Record the last parameter. If it is not in auto mode, this parameter will not be recorded
//If it is auto mode, then this auto will not be false, it will be an array
fired = true;
for (var i = 0, len = list.length; i < len; i ) {
If (list[i].apply(data[0], data[1]) === false)
break;
                                                                                                   }                                                                                                        }                     };
                   return {
                         add: function (fn) {
If (list) {
//Add a callback function
                                 list.push(fn);
//Automatic execution mode, note that if the auto model
//memory is assigned in coreFire(), the default is false
If (memory) coreFire(auto);
                                                                                                       } //Support chain callbacks
                                                                                         return this;                    },
fireWith: function (context, data) {
If (once) list = [];
//Call coreFire here and convert the parameters into an array
coreFire([context, data]);
                                                                                         return this;                  }
                                                                                                                                                                                                                                                                                                   };
            };
               window.Callbacks = Callbacks;
         }(window));


We saw in the code of the previous auto implementation that we lost the Context. jQuery fixed this bug early in fireWith() - fix the parameters in fireWith(). jQuery extracts the logic that should execute the function in fireWith(). We temporarily name it coreFire(). In the original fireWith(), the parameters are spliced ​​into an array: the first parameter represents the context, and the second parameter Parameters represent the parameters passed in. Then execute coreFire().


During add(), jQuery did not assign a value to the variable auto(memory), but chose to assign a value to auto(memory) in coreFire(), thus ensuring that it will not be turned on until the first fire(). Automatically executed.


As mentioned above, the parameters received by coreFire() are actually an array. The first parameter is the context, and the second parameter is the parameter passed in from the outside. At the same time, assign this array to auto (memory), so that the definition of the variable auto (whether to execute the mode automatically) becomes memory (memory of the last parameter passed).

It's really a brilliant idea that kills two birds with one stone. I have to like it. I define this as auto because it is an automatically executed model, and by the way, the parameters of the last fire() are saved. The definition of jQuery as memory may be the author's sigh for the miraculous workmanship here.


As for once&auto, it just combines these two codes. You only need to determine in coreFire() that if it is auto mode, then reset the list to a new array, otherwise set it to undefined directly.

Source code

This code is handwritten by me corresponding to jQuery. Some jQuery public functions are written in. It is not a code fragment, so it can be directly referenced and run.

Copy code The code is as follows:

(function (window, undefined) {
/*
* A callback function tool object. Note that the array will be cleared after the work object is completed:
* * Provides a common set of APIs, but it has the following working model -
*ONCE -Single execution model: every time I work, no longer work
*Auto -Automatic execution model: Each adding a callback function is added to automatically execute all the callback functions in the existing callback function set, and the parameters of this time are passed to all the callback functions
*
*/

//Tool function
var isIndexOf = Array.prototype.indexOf, //Es6
        toString = Object.prototype.toString, //Cache toString method
TosLice = Array.prototype.slice, // Caches Slice method
                                                                                                                                                                                                                     return "object" === typeof document.getElementById ?
               isFunction = function (fn) {
                      //There is a problem with the recognition of DOM and BOM under ie
                  try {
Return /^s*bfunctionb/.test("" fn);
                        } catch (x) {
Return false
                }
              } :
               isFunction = function (fn) { return toString.call(fn) === '[object Function]'; };
         })(),
                                                                                                                                                                                                                   //The first parameter represents the array to be looped, and the second parameter is the function executed each time through the loop
If (arguments.length < 2 || !isFunction(arguments[1])) return;
                       //Why is slice invalid? ?
          var list = toSlice.call(arguments[0]),
                             fn = arguments[1],
item;
​​​​​​while ((item = list.shift())) {//No direct determination of length, speed up
// Why use call here, and Apply is not okay?
                               // Done - the second parameter of apply must be an array object (there is no verification whether array-like is possible, and call does not have this requirement)
                                                                          //apply is described as follows: If argArray (the second parameter) is not a valid array or is not an arguments object, a TypeError will be caused.
                           fn.call(window, item);
            }
},
inArray = function () { //Detect whether the array contains an item and return the index of the item
                                                                                                                                                                                  // Pre-compilation
                 return isIndexOf ? function (array, elem, i) {
If (array)
                          return isIndexOf.call(array, elem, i);
                    return -1;
               } : function (elem, array, i) {
              var len;
                     if (array) {
                    len = array.length;
                    i = i ? i < 0 ? Math.max(0, len i) : i : 0;
                    for (; i < len; i ) {
                        if (i in array && array[i] === elem) {
                            return i;
                        }
                    }
                }
                return -1;
            }
        }();

var Callbacks = function (option) {
          option = toString.call(option) === '[object Object]' ? option: {};
​​​​ //Use closures because each new callbacks has its own state
        var list = [],                                                         var list = [],                                                                                                 var list =                               _list = [],        // If this callbacks object is locked, clear the list and put the original list into _list
                                                                                                                                                                                     haven have been been been executed
firingStart, //Function index (starting point) executed by the current callback function list
              firingLength,                                                                        // Array length of the callback function
                                                                                                                                                                                                                                               .                              // The usage of this variable is very strange and sharp, it not only contains the flag of whether to specify execution, but also records the data
//This auto is simply crazy when used with once: [For the first time] it will be automatically executed after executing fire. With once, it can be done: once executed, no code will be appended or executed later, ensuring the stability and stability of a set of callback data. Safe
stack = !option.once && [], //A callbacks stack. If the callback array is currently being executed and a new callback function is added during execution, then the new callback function will be pushed into the callback array. Stack
            firing = false, //Whether callbacks are working/executing
//Trigger callback function
fire = function (data) {
//Note that this data is an array. If auto mode is configured, auto will never be false because auto will be an array
                    auto = option.auto && data; //Here, if the configuration requires memorizing the last parameter, then remember this parameter (very sharp usage, directly fetch the data)
fired = true;
firingIndex = firingStart || 0;
                firingStart = 0;//Clear firingStart (if you don’t clear it, there will be problems in the next execution)
Firinglength = list.length; // cache list length, the outside world can access
                  firing = true; // Executing callback function
for (; firingIndex < firingLength; firingIndex ) {
If (list[firingIndex].apply(data[0], data[1]) === false) {
                                                                          // Note, if option.auto (automatic execution) is configured, and there is a function in the stack (stack), then there is a section of code in the add() code that will directly execute this method for auto judgment
//We want to block that code, so set auto to false
                                  auto = false;
                          break;
                                    }//When the function returns false, terminate the execution of the subsequent queue
                }
                firing = false; // Flag status has been executed callback function [the function in the stack (stack) has not yet been executed]
//If this stack is not configured once, it must be [], so there must be
//The main function here is that if once is not configured, the following code will be intercepted. If once is configured, the data will be cleared after executing the code
                     if (stack) {
If (stack.length) // First intercept the code of the listing state below, and then determine whether there is a stack
Fire (stack.shift ()); // Take it out of the head of the stack and recurs the FIRE () method
                }
Else if (auto) // code came here, proves that it has been configured Option.once (only once executed), so the list is clear
                   list = [];
Else // proves that there is no configuration AUTO, but ONCE is configured, so the sacrifice is the ultimate Dafa, and the callbacks object is directly abolished
                        self.disable();
            };
      var self = {
               add: function () {//Add a callback function
                        if (list) {
                  var start = list.length;
(function addCallback(args) {
each(args, function (item) {
If (isFunction(item)) {//If it is a function, push the callback list
                                                                                                                                                    //Note that typeof and Object.prototype.toString are different
                              } else if (toString.call(item) === '[object Array]') {//If it is an array, recursively push into the callback list, this judgment abandons array-like
                                                                                                                                                            addCallback(item);
                                                                                                   }                               });
                         })(arguments);
                }
If (firing)//If a callback function is currently executing, then the length of the current callback function list needs to be updated, otherwise the newly pushed callback function will be passed over.
firingLength = list.length;
                  else if (auto) {//If the callback function is not currently executed and automatic execution is required
                                                                                                                                                                                                                              // Note that the value is assigned to firingStart. The fire method above is using firingIndex. This will not affect the execution line of the above code
firingStart = start;
//Execute our newly added partners
fire(auto);
                }
                   return this;
            },
               fire: function () {//Trigger callback function
                     self.fireWith(this, arguments);
                   return this;
            },
​​​​​​ fireWith: function (context, args) {//Trigger the callback function and specify the context
//If once is configured, stack will be undefined, and once needs to be guaranteed to be executed only once, so once executed once, the code here will not be executed again
If (list && (!fired || stack)) {
//Correction parameters
//Here, the context index is 0
//The parameter list index is 2
// Converting to array access is because object representation consumes more resources. There is auto [memory parameters, automatic execution] function in the top-level fire() code. If objects are used, more memory will be consumed
                     args = [context,
                                                                                                                                      args.slice && args.slice()
                                                                                                                                                                          ];
fire(args);
                }
                   return this;
            },
               remove: function () {//Remove a callback function
                        if (list) {
each(arguments, function (item) {
                      var index;
                                        // There may be multiple items, the index can represent the search range in the loop, and the previously searched items do not need to be searched again
While ((index = inArray(item, list, index)) > -1) {
                                           list.splice(index, 1);
If (firing) {
//Ensure that the function list being executed in fire above can run correctly. These global variables are set in fire so that they can be removed asynchronously
If (index <= firingLength)//Correction length
firingLength--;
If (index <= firingLength)//Correction index
firingIndex--;
                                                                                                   }                                                                                                        }                      });
                }
                   return this;
            },
Has: function (fn) {//Whether it contains a callback function
                                                                                                                    return fn ? inArray(fn, list) > -1 : list && list.length;
            },
              empty: function () {//Empty this callbacks object
list = [];
firingLength = 0;
                   return this;
            },
                disable: function () {//Destroy this callbacks object, and the subsequent callback function list will no longer be executed
                   list = stack = auto = undefined;
                   return this;
            },
              disabled: function () {//Whether it has been disabled
 

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn