jQuery source analysis of Callbacks details

  • 2020-05-16 06:16:46
  • OfStack

The nature of the code highlights the concept of order, especially in javascript -- after all, javascript is a single-threaded engine.

javascript has functional programming features, and because of the javascript single-threaded engine, our functions always need to be executed in order. Good code often cuts functions into modules and executes them under certain conditions. Since these functions are executed in an orderly fashion, why don't we write an object that is managed by one to help us manage these functions -- Callbacks (callback function) is born.

What is a Callbacks

javascript is full of function programming. For example, the simplest window.onload accepts one function, while the sad thing is that window.onload can only accept one function if it is directly assigned. If there are several functions that want to be executed in onload, we need to write the following code:


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

The callback function was originally built on this thing, so instead of dispersing these functions, we're going to put them all together. As you can see, in window.onload we want to do two things with an Element: first change the html structure, and then change the style of this html. Both functions are also for an Element operation, and the final execution of both functions is orderly. So why don't we write one of these objects to manage these functions. Of course, this is only the most basic significance of callback function, we need more than 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 other than just one of them executing the function.

The essence of Callbacks is to control the orderly execution of functions. Javascript is a single-threaded engine, which means that javascript only has one code running at a time -- even if it is Ajax, setTimeout. These two functions look like you are asynchronous, in fact is not the case, the browser when running javascript code, the code will be ordered into a queue, when you run Ajax, the browser will pressed Ajax queue into the code, the browser to handle javascript code from the code 1 1 in the queue in code execution - Callbacks, catered to the single thread engine.

Of course, we want more than just a simple tool object -- in the jQuery source code, Callbacks provides the basic management of a set of functions that provide the basis for Deferred (asynchronous queues) and also serve Queue (synchronous queues). Deferred is used for flat/flat pyramid programming (lots of nested callback functions, such as the code that needs to be executed based on the requested return code in Ajax); And Queue drives jQuery.animate (animation engine).

So let's write one Callbacks.

Callbacks model

Array (array) :
Since we have Callbacks to carry on the 1 series of functions, we must have a container. We can take an array, press each function into the array, and loop through the array items when we need to execute.

Working model:

The Callbacks needs to be very powerful, and not just push in the function and execute it, the 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 one execution. We can provide a stable and effective scheme for users of the Callbacks object to ensure that functions will only be executed once and will not be executed again, thus stabilizing the threads of these functions.
auto: automatic execution model, this is an interesting model, some functions rely on 1 layer, such as function b a implementation depends on the function, and then we provide an automated model: first perform the Callbacks, after each time a function is added to the Callbacks, automatically add the function of the past, and the last one for a given parameter data passed to the function in the past, so it is erased from the Callbacks requires repeated trigger the relationship between the dependent function, this is an interesting model.
once & auto: we can make it stronger, work once and auto model at the same time, namely: when the each function is added to the Callbacks, function is performed in the past, then, release the function in the past, continue to add next time function, the past will not perform the function, because once model, has been released them.

API:

add(function) - add 1 (or more) functions to Callbacks objects: of course, if you don't add functions just to see Callbacks in curiosity, we'll let you keep having fun -- we don't throw exceptions, because we're not very good at it.
remove(function) - remove 1 function from an Callbacks: now that we have an addition, we should also offer the option of going back on it, how approachable we are, tolerating what others have done in the past.
has(function) - determines if Callbacks contains 1 function: oh? You're not sure if this function is included. You threw it in the first place. Why are you so careless? But since you asked me, I'll still tell you if Callbacks includes this function. I know you're busy and can't remember and be sure of everything.
empty() - empty Callbacks: are these functions meaningless to you? What? You don't want it after you've done it? So you want to clear it out? Well, I'll bear with you for the sake of memory.
disable() - scrap 1 Callbacks: I sacrifice myself for a stable existence with someone else's code - yes, this method can scrap Callbacks completely, as if it had never existed before.
disabled() - determine if the Callbacks has been deactivated: if you still don't believe that the Callbacks is truly self-sacrificing, then this method will give you peace of mind.
lock(boolean) - locks the Callbacks object: you're afraid it's unstable, but you don't want to give it up. lock is a good way to do this.
fire(data) - performs this function in Callbacks: is it not for the fate of this moment that we do all the cuts? The arguments will be the arguments to the functions that need to be executed.
fireWith(context,data) - performs the functions in Callbacks and specifies the context. In fire(), the Context(context) of all functions is an Callbacks object, while fireWidth() allows you to redefine the context of the functions to be executed.
fired() - determine whether this Callbacks has been executed in the past: we believe that most of the time you don't know what you've done in the past, but we have a record of what you've done. If you've executed this Callbacks object in the past, you can't deny it, because we know if you've executed this Callbacks object in the past.

Basic module implementation

Simple implementation:
Let's start with a simple implementation of 1 Callbacks:


(function (window, undefined) {
            var Callbacks = function () {
                // These private variables are protected by closures
                var list = [],// Callback function list
                    fired;// Have you ever executed
                // return 1 A closure Callbakcs object
                return {
                    add: function (fn) {
                        // when Callbacks When it's discarded, list for undefined
                        if (list) {
                            // add 1 The callback function
                            list.push(fn);
                            // Support for chain callback
                        }
                        return this;
                    },
                    fireWith: function (context, data) {
                        // Triggers the callback function and specifies the context
                        if (list) {
                            fired = true;
                            for (var i = 0, len = list.length; i < len; i++) {
                                // when Callbacks In a certain 1 Function return false When, stop Callbacks Subsequent execution
                                if (list[i].apply(context, data) === false)
                                    break;
                            }
                        }
                        return this;
                    },
                    fire: function () {
                        // Triggers the callback function
                        // call fireWith And specify the context
                        return this.fireWith(this, arguments);
                    },
                    empty: function () {
                        // empty list Can be
                        if (list)// When the Callbacks When it's discarded, Callbacks It should not continue to be used
                            list = [];
                        return this;
                    },
                    disable: function () {
                        // Scrap the Callbacks Object, the subsequent list of callback functions is no longer executed
                        list = undefined;
                        return this;
                    },
                    disabled: function () {// To detect the Callbacks Whether it has been eliminated
                        // convert boolean return
                        return !list;
                    },
                    fired: function () {// this callbacks Have you ever executed
                        return !!fired;
                    }
                };
            };
            // Registered to window Under the
            window.Callbacks = Callbacks;
        }(window));

Then let's test 1 for this Callbacks:


        var test = new Callbacks();
        test.add(function (value) {
            console.log(' function 1 . value Is this: ' + value);
        });
        test.add(function (value) {
            console.log(' function 2 . value Is this: ' + value);
        });
        test.fire(' This is the function 1 And the function 2 The value of the ');
        console.log(' Check if the function has been executed: ' + test.fired());
        test.disable();// Scrap the Callbacks
        console.log(' See if the function is deprecated: ' + test.disabled());
        test.add(function () {
            console.log(' Add the first 3 This function should not be executed ');
        });
        test.fire();

Open the browser's console and you'll see that it works.

once and auto(memory) implementations

once:
once makes this function in callbacks run once and then it doesn't run again. The principle is very simple. In the above code, we can see that there is a variable list subscript function list, so we only need to clean up the code that has been executed in the past. We use a global variable to save the current execution model. If it is an once model, disable this list in fireWith() :


(function (window, undefined) {
            var Callbacks = function (once) {
                // These private variables are protected by closures
                var list = [],// Callback function list
                    fired;// Have you ever executed
                // return 1 A closure Callbakcs object
                return {
                    //... Omitted code
                    fireWith: function (context, data) {
                        // Triggers the callback function and specifies the context
                        if (list) {
                            fired = true;
                            for (var i = 0, len = list.length; i < len; i++) {
                                // when Callbacks In a certain 1 Function return false When, stop Callbacks Subsequent execution
                                if (list[i].apply(context, data) === false)
                                    break;
                            }
                        }
                        // If configured once Model, then global variables once for true , list reset
                        if (once) list = undefined;
                        return this;
                    }
                    //... Omitted code
                };
            };
            // Registered to window Under the
            window.Callbacks = Callbacks;
        }(window));

auto:

auto (memory) model in jQuery was named for memory, was originally the name to confuse, studying the usage before deciding to auto - its role is "the first fire (), after the follow-up add () function of the automatic execution", of the following can be used: when add 1 set of functions to Callbacks, temporary and need to add a function, then the real-time running this new additional function, have to say, in order to ease of use, this pattern is a little hard to understand. The implementation is to determine if the auto model is add(), and if the auto model is add, the function is executed. However, we need to execute fire() after the first fire(), Callbacks without fire() should not be executed automatically, and after each automatic execution, we need to pass the parameters used in the last time to the automatic function.

You might think of the following code:


(function (window, undefined) {
            var Callbacks = function (once, auto) {
                var list = [],
                    fired,
                    lastData;// Save the last 1 Parameter for the second execution
                return {
                    add: function (fn) {
                        if (list) {
                            list.push(fn);
                            // - Automatic execution mode
                            // The last 1 The parameter used for the second time is passed over, and is lost here Context( context )
                            // In order not to lose context here, we may also need to declare 1 Three variables save at the end 1 Time to use Context
                            if (auto) this.fire(lastData);
                        }
                        return this;
                    },
                    fireWith: function (context, data) {
                        if (list) {
                            lastData = data;// - Record the last 1 The parameter used for the second time
                            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;
                    }
                    // Partial code omission
                };
            };
            // Registered to window Under the
            window.Callbacks = Callbacks;
        }(window));

But in jQuery there is a more curious use of jQuery, and the authors are also proud of this use, so they named the model memory -- so that the variable auto above not only indicates that auto is currently in auto execution mode, but also serves as a container for the last argument, which represents both auto and memory. (the following code not jQuery is written according to the jQuery code idea, not the source code) :


(function (window, undefined) {
            var Callbacks = function (auto) {
                var list = [],
                    fired,
                    memory,// The lead actor is here, yes memory
                    coreFire = function (data) {
                        // The actual trigger function method
                        if (list) {
                            //&& Expression magic
                            memory = auto && data;// Record the last 1 The argument to the second if not auto The schema does not record this parameter
                            // If it is auto Pattern. So this auto Will not be false , it would be 1 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 1 The callback function
                            list.push(fn);
                            // Automatic execution mode, note if auto model
                            //memory Is in the coreFire() The default is false
                            if (memory) coreFire(auto);
                        }
                        // Support for chain callback
                        return this;
                    },
                    fireWith: function (context, data) {
                        if (once) list = [];
                        // This call coreFire , converted the parameters to an array
                        coreFire([context, data]);
                        return this;
                    }
                    /* Partial code omission */
                };
            };
            window.Callbacks = Callbacks;
        }(window));

We saw in the last auto implementation that we lost Context, and jQuery fixed this bug in fireWith() -- the parameter was fixed in fireWith(). jQuery takes the logic out of fireWith() that is supposed to perform the function, and we temporarily call it coreFire(). In fireWith(), we concatenate the parameters into an array: the first parameter represents the context, and the second parameter represents the parameters passed in. Then coreFire() is executed.

Instead of assigning auto (memory) a value at add(), jQuery selects auto (memory) in coreFire(), thus ensuring that fire() is automatically enabled after the first fire().

As stated above, coreFire() receives an array of parameters, the first of which is the context, and the second of which is the parameter passed in from the outside. This array is also assigned to auto (memory), so that the definition of the variable auto (whether it is in automatic mode or not) becomes memory (remember the parameter passed the last time).
What a 1 stone 2 bird god thought, god thought, had to thumb up. I define this as auto because it is itself an automatically executed model that holds the parameters of fire() for the last time, and jQuery as memory is perhaps the author's surprise here.

As for once&auto, it is only necessary to merge the two codes into 1, just decide in coreFire() that if it is auto mode, then reset list to a new array, otherwise, it is simply set to undefined.

The source code

This code is my own hand-written copy of jQuery, which includes some public functions of jQuery. It is not a snippet of code, so it can be directly referenced to run.


(function (window, undefined) {
    /*
    * 1 Callback function utility object. Notice that this work object empties the array when the work is done :
    *   provide 1 Set of normal API , but it has the following working model -
    *                     once - Single execution model: each job 1 Next, no more work
    *                     auto - Automatic execution model: per addition 1 Callbacks, automatically executes all callbacks in the existing set of callbacks, and passes the arguments to all callbacks
    *
    */     // Tool function
    var isIndexOf = Array.prototype.indexOf,    //Es6
        toString = Object.prototype.toString,   // The cache toString methods
        toSlice = Array.prototype.slice,        // The cache slice methods
        isFunction = (function () {             // determine 1 Is the object Function
            return "object" === typeof document.getElementById ?
            isFunction = function (fn) {
                //ie Under the DOM and BOM There is a problem with identification
                try {
                    return /^\s*\bfunction\b/.test("" + fn);
                } catch (x) {
                    return false
                }
            } :
            isFunction = function (fn) { return toString.call(fn) === '[object Function]'; };
        })(),
        each = function () {                    // Loop through the method
            // The first 1 Two parameters represent the array to be looped, number 2 Three parameters are the functions that are executed each time in a loop
            if (arguments.length < 2 || !isFunction(arguments[1])) return;
            // why slice Invalid??
            var list = toSlice.call(arguments[0]),
                fn = arguments[1],
                item;
            while ((item = list.shift())) {// There is no direct determination length And speed up
                // Why is it used here call You can do that apply Is not?
                // done - apply The first 2 All the parameters have to be 1 a array Object (no validation array-like Whether you can, and call There is no such requirement.)
                //apply It goes like this: if argArray (the first 2 A parameter) not 1 A valid array or not arguments Object, then will lead to 1 a TypeError .
                fn.call(window, item);
            }
        },
        inArray = function () {                     // Detects whether an item is included in the array and returns the index
            // precompiled
            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 one callbacks Each has its own state
        var list = [],      // The callback list
            _list = [],     // If I lock this callbacks Object, empty list And the original list placement _list
            fired,          // Have you ever executed
            firingStart,    // Function index of the current callback function list execution (starting point)
            firingLength,   // The array length of the callback function
            auto,   // Whether the flag is automatically executed, and if it is required, then auto Remembering the last 1 Second callback parameter (last 1 time fire , this is 1 It's a weird and bizarre use
            // The use of this variable is weird and sharp, both containing the flag whether or not execution is specified and recording the data
            // this auto Cooperate with once It's crazy: no 1 Time] implemented fire After the automatic implementation, cooperate once Can be done: 1 Second execution, no more appended and executed code, guaranteed 1 Stability and security of group callback data
            stack = !option.once && [],     //1 a callbacks Stack, if the callback array is currently being executed and a new callback function is added to the execution, the new callback function will be pushed onto the stack
            firing = false, //callbacks Are you working / perform
        // Triggers the callback function
            fire = function (data) {
                // Pay attention to this data It's an array, if configured auto Pattern, so auto Never for false Because the auto It's going to be an array
                auto = option.auto && data; // In this case, if the configuration requires you to remember the last parameter, remember this parameter (very sharp usage, directly fetch the data)
                fired = true;
                firingIndex = firingStart || 0;
                firingStart = 0;// empty firingStart (not empty next time there is a problem with execution)
                firingLength = list.length;         // The cache list Length, accessible to the outside world
                firing = true; // The callback function is executing
                for (; firingIndex < firingLength; firingIndex++) {
                    if (list[firingIndex].apply(data[0], data[1]) === false) {
                        // Note that if configured option.auto (automatically), and stack There's a function in the stack, so add() In the code 1 Period for auto Determine the code that will execute this method directly
                        // We're going to block that code, so set it auto for false
                        auto = false;
                        break;
                    }// When the function returns false , terminates the execution of the subsequent queue
                }
                firing = false; // Indicates that the status has completed the execution of the callback function [stack( The stack ) The inside function has not yet been executed ]
                // If the stack is not configured at all once Certainly in the case of [] , so 1 Set there
                // The main effect here is if there is no configuration once , intercepts the following code if configured once , empty the data after executing the code
                if (stack) {
                    if (stack.length)// So let's clear the bottom list The state of the code to intercept, and then determine whether there is a stack
                        fire(stack.shift()); // Pull from the top of the stack, and recurse fire() methods
                }
                else if (auto)    // The code goes here to prove that it is configured option.once (only perform 1 Time), then list empty
                    list = [];
                else                // Prove that there is no configuration auto , but configured once Then sacrifice the ultimate dharma, directly nullify this callbacks object
                    self.disable();
            };
        var self = {
            add: function () {// add 1 The callback function
                if (list) {
                    var start = list.length;
                    (function addCallback(args) {
                        each(args, function (item) {
                            if (isFunction(item)) {// Is a function, then presses the callback list
                                list.push(item);
                                // Pay attention to typeof and Object.prototype.toString It is not 1 The sample of
                            } else if (toString.call(item) === '[object Array]') {// If it is an array, the callback list is pushed recursively, and the decision is discarded array-like
                                addCallback(item);
                            }
                        });
                    })(arguments);
                }
                if (firing)// If a callback function is currently being executed, you need to update the current callback function list length , otherwise the newly pressed callback function will be skipped.
                    firingLength = list.length;
                else if (auto) {// If the callback function is not currently executed, it is required to be executed automatically
                    // Notice this is giving firingStart Assignment, up here fire The method is being used firingIndex , this will not affect the execution line of the above code
                    firingStart = start;
                    // Execute our new addition
                    fire(auto);
                }
                return this;
            },
            fire: function () {// Triggers the callback function
                self.fireWith(this, arguments);
                return this;
            },
            fireWith: function (context, args) {// Triggers the callback function and specifies the context
                // If configured once . stack Will provide the undefined And the once Again, you need to make sure that you only execute 1 Time, so 1 Once carried out 1 Again, this code will not be executed
                if (list && (!fired || stack)) {
                    // Correction parameters
                    // Here, ,context The index for 0
                    // The parameter list index is 2
                    // The conversion to array access is because the object represents more resource consumption at the top level fire() In the code auto[ Memory parameters, automatic execution ] This feature USES more memory if objects are used
                    args = [context,
                        args ?
                        args.slice && args.slice()
                        || toSlice.call(args) :
                        []
                    ];
                    fire(args);
                }
                return this;
            },
            remove: function () {// remove 1 The callback function
                if (list) {
                    each(arguments, function (item) {
                        var index;
                        // There may be multiple, index You can represent the scope of the retrieval in a loop, and the previous retrieval can be done without retrieval
                        while ((index = inArray(item, list, index)) > -1) {
                            list.splice(index, 1);
                            if (firing) {
                                // Ensure that the above fire The list of functions being executed in fire This is where you can remove them asynchronously
                                if (index <= firingLength)// Fixed length
                                    firingLength--;
                                if (index <= firingLength)// Modified index
                                    firingIndex--;
                            }
                        }
                    });
                }
                return this;
            },
            has: function (fn) {// Does it include 1 The callback function
                return fn ? inArray(fn, list) > -1 : list && list.length;
            },
            empty: function () {// Clear the callbacks object
                list = [];
                firingLengt

Related articles: