jQueryのCallback実現

45013 ワード

js開発では,マルチスレッドがないため,ready関数にコールバック関数を登録したり,要素を登録したりするイベント処理など,コールバックという概念に遭遇することが多い.複雑なシーンでは、1つのイベントが発生した場合、複数のコールバックメソッドを同時に実行する必要がある場合があります.直接考慮できる実装は、すべてのイベントがトリガーされたときにコールバックする必要がある関数をこのキューに追加して保存し、イベントがトリガーされたときに、このキューから保存された関数を順番に取り出して実行することです.
以下のように簡単に実現できる.
まず,このコールバッククラスを表すクラス関数を実現する.javascriptでは、配列を使用してこのキューを表します.
function Callbacks() {
    this.list = [];
}

次に,クラス内のメソッドをプロトタイプにより実現する.追加および削除された関数は配列に保存され、fireの場合、各コールバック関数に渡されるパラメータを提供できます.
Callbacks.prototype = {
    add: function(fn) {
        this.list.push(fn);
    },
    remove: function(fn){
        var position = this.list.indexOf(fn);
        if( position >=0){
            this.list.splice(position, 1);
        }
    },
    fire: function(args){
        for(var i=0; i<this.list.length; i++){
            var fn = this.list[i];
            fn(args);
        }
    }
};

テストコードは次のとおりです.
function fn1(args){
    console.log("fn1: " + args);
}

function fn2(args){
    console.log("fn2: " + args);
}

var callbacks = new Callbacks();
callbacks.add(fn1);
callbacks.fire("Alice");

callbacks.add(fn2);
callbacks.fire("Tom");

callbacks.remove(fn1);
callbacks.fire("Grace");

あるいは,プロトタイプを用いず,直接閉パケットにより実現する.
function Callbacks() {
    
    var list = [];
    
    return {
         add: function(fn) {
            list.push(fn);
        },
        
        remove: function(fn){
            var position = list.indexOf(fn);
            if( position >=0){
                list.splice(position, 1);
            }
        },
        
        fire: function(args) {
            for(var i=0; i<list.length; i++){
                var fn = list[i];
                fn(args);
            }    
        }
    };
}

そうすると、サンプルコードも調整する必要があります.Callbacks関数を直接使えばいいです.
function fn1(args){
    console.log("fn1: " + args);
}

function fn2(args){
    console.log("fn2: " + args);
}

var callbacks = Callbacks();
callbacks.add(fn1);
callbacks.fire("Alice");

callbacks.add(fn2);
callbacks.fire("Tom");

callbacks.remove(fn1);
callbacks.fire("Grace");

次に、2つ目の方法を使用して続けます.
より複雑なシーンでは、fireを1回だけ呼び出す必要があります.後でfireを呼び出しても、有効になりません.
たとえば、オブジェクトを作成するときに、このような形式になる可能性があります.ここでonceはfireが1回しかできないことを表す.
var callbacks = Callbacks("once");

では、私たちのコードも調整する必要があります.実は簡単で、onceが設定されていれば、fireの後、元のキューをそのまま乾かすだけでいいのです.
function Callbacks(options) {
    var once = options === "once";
    var list = [];
    
    return {
         add: function(fn) {
            if(list){
                list.push(fn);
            }
        },
        
        remove: function(fn){
            if(list){
                var position = list.indexOf(fn);
                if( position >=0){
                    list.splice(position, 1);
                }
            }
        },
        
        fire: function(args) {
            if(list)
            {
                for(var i=0; i<list.length; i++){
                    var fn = list[i];
                    fn(args);
                }
            }
            if( once ){
                list = undefined;
            }
        }
    };
}

jQueryでは、onceの1つの方法だけでなく、4つのタイプの異なる方法を提供しています.
  • once:一度だけトリガーできます.
  • memory:キューがトリガーされた後、追加された関数は直接呼び出され、もう一度トリガーする必要はありません.
  • unique:保証関数の一意
  • stopOnFalse:コールバックがfalseに返されると、後続のコールが中断されます.

  • この4つの方法は、入力構造関数、例えば$をスペースで区切ることで組み合わせることができる.Callbacks("once memory unique");
    公式ドキュメントでは、使用例をいくつか示しています.
    callbacks.add(fn1, [fn2, fn3,...])//1つ/複数のコールバックcallbacksを追加する.remove(fn1, [fn2, fn3,...])//1つ/複数のコールバックcallbacksを削除する.Fire(args)/コールバックをトリガーし、argsをfn 1/fn 2/fn 3に渡す......callbacks.FireWith(context,args)/コンテキストcontextを指定してコールバックcallbacksをトリガーする.ロック()/キューの現在のトリガ状態をロックcallbacks.disable()/マネージャを無効にします.つまり、すべてのfireが有効になりません.
    コンストラクション関数列は実際には文字列であるため,まずこの列を解析し,使いやすいオブジェクトとして構築する必要がある.
    // String to Object options format cache
    var optionsCache = {};
    
    // Convert String-formatted options into Object-formatted ones and store in cache
    function createOptions( options ) {
        var object = optionsCache[ options ] = {};
        jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) {
            object[ flag ] = true;
        });
        return object;
    }

    この関数は、次のパラメータ「once memory unique」をオブジェクトに変換します.
    {
        once: true,
        memory: true,
        unique: true
    }

    さらに、fire処理キューで、ある関数がキューにコールバック関数を追加したり、キューにコールバック関数を削除したりするなど、特殊な状況を考慮します.このような状況を処理するために、キュー全体を巡回する過程で、現在の処理の開始ダウンスケール、現在の処理の位置などの情報を記録することができ、これにより、同時のような状況を処理することができます.
    // Flag to know if list was already fired
    fired,
    // 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,
    // Actual callback list
    list = [],

    もしfire処理中に、ある関数がfireを呼び出してイベントをトリガーしたら?
    このネストされたイベントを先に保存し、現在のコールバックシーケンスの処理が完了するまで待ってから、保存されたイベントをチェックし、処理を続行することができます.明らかに、キューを使用することは、このような状況を処理する理想的なデータ構造であり、このような状況に遭遇した場合、イベントデータをキューに入れ、処理待ちの場合、順次キューデータを処理する.いつこのような処理が必要ですか?明らかにonceの状況ではありません.JavaScriptでは、スタックキューも配列によって実現され、pushは配列の最後にデータを追加し、shiftはデキューに使用され、配列の一番前からデータを取得します.
    ただし、jQueryはキューではなくstackと呼ぶ.
    // Stack of fire calls for repeatable lists
    stack = !options.once && [],

    エンキュー・コード.
    if ( firing ) {
        stack.push( args );
    } 

    デキュー・コード
    if ( list ) {
        if ( stack ) {
            if ( stack.length ) {
                fire( stack.shift() );
            }
        } else if ( memory ) {
            list = [];
        } else {
            self.disable();
        }
    }

    まず基本構造を定義し、関数の開始は私たちが使用する変数を定義します.
    jQuery.Callbacks = function( options ) {
      var options = createOptions(options);
     
      var 
        memory,
    
        // Flag to know if list was already fired
        //      fire  
        fired,
        // Flag to know if list is currently firing
        //         firing    
        firing,
        // First callback to fire (used internally by add and fireWith)
        // fire        
        firingStart,
     
        // End of the loop when firing
        //    fire        
        firingLength,
     
        // Index of currently firing callback (modified by remove if needed)
        //      firing          
        firingIndex,
     
        // Actual callback list
        //     
        list = [],
     
        // Stack of fire calls for repeatable lists
        //      once  ,stack         ,         fire          
        stack = !options.once && [],
     
        _fire = function( data ) {
        };
     
      var self = {
        add : function(){},
        remove : function(){},
        has : function(){},
        empty : function(){},
        fireWith : function(context, args){
            _fire([context, args]);
        };
        fire : function(args){
            this.fireWith(this, args);
        }
        /* other function */
      }
      return self;
    };

     
    その中のstackはfireの後に追加された関数を保存するために使用されます.
    firingIndex、firingLengthは関数を呼び出す過程で、このキューを操作することもできることを保証するために使用されます.同時処理を実現する.
    add関数から始めます.
    add: function() {
        if ( list ) {  //       once,       ,         。
            // First, we save the current length,          
            var start = list.length;
            (function add( args ) {
                jQuery.each( args, function( _, arg ) {  
                    var type = jQuery.type( arg );
                    if ( type === "function" ) {
                        if ( !options.unique || !self.has( arg ) ) {
                            list.push( arg );
                        }
                    } else if ( arg && arg.length && type !== "string" ) {
                        // Inspect recursively
                        add( arg );
                    }
                });
            })( arguments );
            // Do we need to add the callbacks to the
            // current firing batch?    firing  ,         
            if ( firing ) {
                firingLength = list.length;
            // With memory, if we're not firing then
            // we should call right away     memory   ,        ,    , memory            
            } else if ( memory ) {
                firingStart = start;
                fire( memory );
            }
        }
        return this;
    },

    削除は簡単です.削除の準備をしている関数がキューにあるかどうかを確認します.whileの役割は、コールバックがキューに複数回追加される可能性があることです.
    // Remove a callback from the list
    remove: function() {
        if ( list ) {
            jQuery.each( arguments, function( _, arg ) {
                var index;
                while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
                    list.splice( index, 1 );
                    // Handle firing indexes
                    if ( firing ) {
                        if ( index <= firingLength ) {
                            firingLength--;
                        }
                        if ( index <= firingIndex ) {
                            firingIndex--;
                        }
                    }
                }
            });
        }
        return this;
    },

    has,empty,disable,disabledは比較的簡単である.
    // Check if a given callback is in the list.
    // If no argument is given, return whether or not list has callbacks attached.
    has: function( fn ) {
        return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length );
    },
    // Remove all callbacks from the list
    empty: function() {
        list = [];
        firingLength = 0;
        return this;
    },
    // Have the list do nothing anymore
    disable: function() {
        list = stack = memory = undefined;
        return this;
    },
    // Is it disabled?
    disabled: function() {
        return !list;
    },

    ロックとは、イベントを再トリガすることは許されないという意味で、stack自体もイベントを再トリガすることを禁止するかどうかを表すために使用されます.したがって,stackをundefinedに直接設定することで,再トリガイベントの可能性を閉じる.
    // Lock the list in its current state
    lock: function() {
        stack = undefined;
        if ( !memory ) {
            self.disable();
        }
        return this;
    },
    // Is it locked?
    locked: function() {
        return !stack;
    },

    Fireは暴露のトリガ方法である.FireWithでは、現在のコンテキスト、すなわちコールバック関数で使用されるthisを指定できます.第1行のif判定ではイベントをトリガする条件を示し,listが存在しなければならず,stackが存在しなければならないか,まだトリガされていない.
    // Call all callbacks with the given context and arguments
    fireWith: function( context, args ) {
        if ( list && ( !fired || stack ) ) {
            args = args || [];
            args = [ context, args.slice ? args.slice() : args ];
            if ( firing ) {
                stack.push( args );
            } else {
                fire( 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 !!fired;
    }
    };

    本物のfire関数.
    // Fire callbacks
    fire = function( data ) {
        memory = options.memory && data;
        fired = true;
        firingIndex = firingStart || 0;
        firingStart = 0;
        firingLength = list.length;
        firing = true;
        for ( ; list && firingIndex < firingLength; firingIndex++ ) {
            if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {
                memory = false; // To prevent further calls using add
                break;
            }
        }
        firing = false;
        if ( list ) {
            if ( stack ) {
                if ( stack.length ) {
                    fire( stack.shift() );
                }
            } else if ( memory ) {
                list = [];
            } else {
                self.disable();
            }
        }
    },

     
     
    jQuery-2.1.3.jsでのCallback実装.
    /*
     * Create a callback list using the following parameters:
     *
     *    options: an optional list of space-separated options that will change how
     *            the callback list behaves or a more traditional option object
     *
     * By default a callback list will act like an event callback list and can be
     * "fired" multiple times.
     *
     * Possible options:
     *
     *    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)
     *
     *    unique:            will ensure a callback can only be added once (no duplicate in the list)
     *
     *    stopOnFalse:    interrupt callings when a callback returns false
     *
     */
    jQuery.Callbacks = function( options ) {
    
        // Convert options from String-formatted to Object-formatted if needed
        // (we check in cache first)
        options = typeof options === "string" ?
            ( optionsCache[ options ] || createOptions( options ) ) :
            jQuery.extend( {}, options );
    
        var // Last fire value (for non-forgettable lists)
            memory,
            // Flag to know if list was already fired
            fired,
            // 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,
            // Actual callback list
            list = [],
            // Stack of fire calls for repeatable lists
            stack = !options.once && [],
            // Fire callbacks
            fire = function( data ) {
                memory = options.memory && data;
                fired = true;
                firingIndex = firingStart || 0;
                firingStart = 0;
                firingLength = list.length;
                firing = true;
                for ( ; list && firingIndex < firingLength; firingIndex++ ) {
                    if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {
                        memory = false; // To prevent further calls using add
                        break;
                    }
                }
                firing = false;
                if ( list ) {
                    if ( stack ) {
                        if ( stack.length ) {
                            fire( stack.shift() );
                        }
                    } else if ( memory ) {
                        list = [];
                    } else {
                        self.disable();
                    }
                }
            },
            // Actual Callbacks object
            self = {
                // Add a callback or a collection of callbacks to the list
                add: function() {
                    if ( list ) {
                        // First, we save the current length
                        var start = list.length;
                        (function add( args ) {
                            jQuery.each( args, function( _, arg ) {
                                var type = jQuery.type( arg );
                                if ( type === "function" ) {
                                    if ( !options.unique || !self.has( arg ) ) {
                                        list.push( arg );
                                    }
                                } else if ( arg && arg.length && type !== "string" ) {
                                    // Inspect recursively
                                    add( arg );
                                }
                            });
                        })( arguments );
                        // Do we need to add the callbacks to the
                        // current firing batch?
                        if ( firing ) {
                            firingLength = list.length;
                        // With memory, if we're not firing then
                        // we should call right away
                        } else if ( memory ) {
                            firingStart = start;
                            fire( memory );
                        }
                    }
                    return this;
                },
                // Remove a callback from the list
                remove: function() {
                    if ( list ) {
                        jQuery.each( arguments, function( _, arg ) {
                            var index;
                            while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
                                list.splice( index, 1 );
                                // Handle firing indexes
                                if ( firing ) {
                                    if ( index <= firingLength ) {
                                        firingLength--;
                                    }
                                    if ( index <= firingIndex ) {
                                        firingIndex--;
                                    }
                                }
                            }
                        });
                    }
                    return this;
                },
                // Check if a given callback is in the list.
                // If no argument is given, return whether or not list has callbacks attached.
                has: function( fn ) {
                    return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length );
                },
                // Remove all callbacks from the list
                empty: function() {
                    list = [];
                    firingLength = 0;
                    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 ) {
                        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 ( list && ( !fired || stack ) ) {
                        args = args || [];
                        args = [ context, args.slice ? args.slice() : args ];
                        if ( firing ) {
                            stack.push( args );
                        } else {
                            fire( 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 !!fired;
                }
            };
    
        return self;
    };