jQuery 原始碼學習 (三) 回撥函式

Meils發表於2018-06-10

回撥函式

一、概念

回撥函式是一個通過函式指標來呼叫執行的函式,如果你把一個函式的指標作為引數傳遞出去,那麼這個指標呼叫這個函式的時候,我們就說這是回撥函式。回撥函式不是由該函式的實現方直接呼叫,而是在特定的事件或條件發生時由另外的一方呼叫的,用於對該事件或條件進行響應。

好處: 使用回撥函式進行處理,程式碼就可以繼續進行其他任務,而無需空等。實際開發中,經常在javascript中使用非同步呼叫。

  • 非同步回撥
$(document).ready(callback);
$(document).on(‘click’,callback)

$.ajax({
  url: "aaron.html",
  context: document
}).done(function() { 
        //成功執行
}).fail(function() {
        //失敗執行
);


$(`#clickme`).click(function() {
    $(`#book`).animate({
        opacity: 0.25,
        left: `+=50`,
        height: `toggle`
    }, 5000, function() {
        // Animation complete.
    });
});
  • 同步
var test1 = function(callback) {
    //執行長時間操作
    callback();
}
test1(function() {
    //執行回撥中的方法
});
一個同步(阻塞)中使用回撥的例子,目的是在test1程式碼執行完成後執行回撥callback

所以理解回撥函式最重要的2點:

1、一個回撥函式作為引數傳遞給另一個函式是,我們僅僅傳遞了函式定義。我們並沒有在引數中執行函式。我們並不傳遞像我們平時執行函式一樣帶有一對執行小括號()的函式

2、回撥函式並不會馬上被執行,它會在包含它的函式內的某個特定時間點被“回撥”。

二、觀察者模式

在理解jquery的回撥物件之前我們先來學習一下觀察者模式(SB模式):

觀察者模式: 一個物件作為一個特定任務的觀察者,當這個任務出發或者執行完畢之後通知觀察者(Subscriber)。觀察者也可以叫做訂閱者,它指向被觀察者(Publisher),當事件發生時, 被觀察者會通知觀察者

對於$.Callbacks 建立的Callback物件,它的addfire方法就是,其實就是基於釋出訂閱(Publish/Subscribe)的觀察者模式的設計。

// 模擬一下這種模式
function aa() {
    console.log(`aa`);
}
function bb() {
    console.log(`bb`);
}
var m_db = {
    Callbacks: [],
    add: function(fn) {
        this.Callbacks.push(fn);
    },
    fire: function() {
        this.Callbacks.forEach(function(fn){
            fn();
        })
    }
}
m_db.add(aa);
m_db.add(bb);
m_db.fire();
  • 設計原理

開始構建一個存放回撥的陣列,如this.callbacks= [] 新增回撥時,將回撥push進this.callbacks,執行則遍歷this.callbacks執行回撥,也彈出1跟2了。當然這只是簡潔的設計,便於理解,整體來說設計的思路程式碼都是挺簡單的,那麼我們從簡單的設計深度挖掘下這種模式的優勢。


模式的實際使用

// 首先看一個場景
$.ajax({
    url: ``,
    ..
}).done(function(data) {
    // 第一步處理資料
    
    // 第二步處理DOM
    $(`aaron1`).html(data.a)
    $(`aaron2`).html(data.b)
    $(`aaron3`).html(data.c)   
    
    // 其餘處理
    
})

首先,所有的邏輯都寫在done方法裡面,這樣確實是無可厚非的,但是問題就是邏輯太複雜了。Done裡面有資料處理html渲染、還可能有其它不同場景的業務邏輯。這樣如果是換做不同的人去維護程式碼,增加功能就會顯得很混亂而且沒有擴充套件性。

$.ajax({
    url: ``,
    ..
}).done(function(data) {
    // 第一步處理資料
    processData(data);
    // 第二步處理DOM
    processDom(data);
    
    // 其餘處理
    processOther(data);
})

這樣看著時好一些了,通過同步執行來一次實現三個方面的處理,每一方面的處理都提取出來,但是這樣的寫法幾乎就是“就事論事”的處理,達不到抽象複用。

var m_cb = {
    callbacks: [],
    add: function(fn){
        this.callbacks.push(fn);
    },
    fire: function(data){
        this.callbacks.forEach(function(fn){
            fn(data);
        })
    }
}
m_cb.add(function(data){
    // 資料處理
})
m_cb.add(function(data){
    // DOM處理
    
})

m_cd.add(function(data){
    // 其餘處理
})
$.ajax({
    url: ``,
    ...
}).done(function(data){
    m_cd.fire(data);
})

這樣使用了觀察者模式之後是不是感覺好多了呢,設計該模式背後的主要動力是促進形成鬆散耦合。在這種模式中,並不是一個物件呼叫另一個物件的方法,而是一個物件訂閱另一個物件的特定活動並在狀態改變後獲得通知。訂閱者也稱為觀察者,而被觀察的物件稱為釋出者或主題。當發生了一個重要的事件時,釋出者將會通知(呼叫)所有訂閱者並且可能經常以事件物件的形式傳遞訊息。

總之、觀察者模式就是將函式/業務處理管理起來,當一定的事件觸發或者時某一任務執行完畢後,一次性執行。


三、$.Callbacks()

對於$.Callbacks 建立的Callback物件,它的addfire方法就是,其實就是基於釋出訂閱(Publish/Subscribe)的觀察者模式的設計。

$.Callbacks一般的開發者使用的較少,它的開發實現主要時為$.ajax以及$.deferred

jQuery.Callbacksjquery在1.7版本之後加入的,是從1.6版中的_Deferred物件中抽離的,主要用來進行函式佇列的add、remove、fire、lock等操作,並提供once、memory、unique、stopOnFalse四個option進行一些特殊的控制。

這個函式常使用的就是在事件觸發機制中,也就是觀察者設計模式的訂閱和釋出模式中,$.Callbacks主要出現在ajax、deferred、queue中。

  • 下面來仔細分析一下該方法的使用吧
1、先來跑一下流程
function aa() {
    console.log(`aa`);
}
function bb() {
    console.log(`bb`);
}

var cb = $.Callbacks();
cb.add(aa);
cb.add(bb);
cb.fire(); 
// aa
// bb
function fn1(value) {
    console.log(value);
}

function fn2(value) {
    fn1("fn2 says: " + value);
    return false;
}

var cb1 = $.Callbacks();
cb1.add(fn1); // 新增一個進入佇列
cb1.fire(`foo`); // 執行一下
// foo
cb1.add(fn2); // 再添一個
cb1.fire(`bar`); // 一次性執行
// bar
// fn2 says: bar
cb1.remove(fn2); // 移除一個
cb1.fire(`111`); // 執行剩下的那一個
// 111

$.Callbacks()就是一個工廠函式。

  • jQuery.Callbacks() 的 API 列表如下:
callbacks.add()        :回撥列表中新增一個回撥或回撥的集合。
callbacks.disable()    :禁用回撥列表中的回撥。
callbacks.disabled()   :確定回撥列表是否已被禁用。 
callbacks.empty()      :從列表中刪除所有的回撥。
callbacks.fire()       :用給定的引數呼叫所有的回撥。
callbacks.fired()      :訪問給定的上下文和引數列表中的所有回撥。 
callbacks.fireWith()   :訪問給定的上下文和引數列表中的所有回撥。
callbacks.has()        :確定列表中是否提供一個回撥。
callbacks.lock()       :鎖定當前狀態的回撥列表。
callbacks.locked()     :確定回撥列表是否已被鎖定。
callbacks.remove()     :從回撥列表中的刪除一個回撥或回撥集合。
  • 原始碼結構
jQuery.Callbacks = function(options) {
    // 首先對引數進行緩衝
    options = typeof options === "string" ?
        (optionsCache[options] || createOptions(options)) :
        jQuery.extend({}, options);
    // 實現程式碼
    // 函式佇列的處理
    fire = function() {}
    
    // 自身方法
    self = {
        add: function() {},
        remove: function() {},
        has: function(fn) {},
        empty: function() {},
        disable: function() {},
        disabled: function() {},
        lock: function() {},
        locked: function() {},
        fireWith: function(context, args) {},
        fire: function() {},
        fired: function() {}
    };
    
    
    return self;
};
  • 引數處理
// 處理通過空格分隔的字串
var str = "once queue";
var option = {};
$.each(str.match(/S+/g) || [], function (_index, item) {
    option[item] = true;
})
console.log(option);
// {once: true, queue: true}

Callbacks內部維護著一個List陣列。這個陣列用於存放我們訂閱的物件,它是通過閉包來實現長期駐存的。新增回撥時,將回撥push進list,執行則遍歷list執行回撥。

Callbacks 有4個引數。

    1. once 的作用是使callback佇列只執行一次。
var callbacks = $.Callbacks(`once`);

callbacks.add(function() {
  alert(`a`);
})

callbacks.add(function() {
  alert(`b`);
})

callbacks.fire(); //輸出結果: `a` `b`
callbacks.fire(); //未執行
// 來看一下具體怎麼實現
// jQuery是在執行第一個fire的時候直接給清空list列表了,然後在add的地方給判斷下list是否存在,從而達到這樣的處理
function Callbacks(options){
    var list = [];
    var self = {};
    self: {
        add: function(fn){
            list.push(fn);
        },
        fire: function(data){
            this.list.forEach(function(item){
                item(data);
            })
            if(options == `once`) {
                list = undefined;
            }
        }
        
    }
    return self;
}
// $jQuery.Callbacks的處理,在fire中呼叫了 self.disable(); 方法
// 禁用回撥列表中的回撥。
disable: function() {
    list = stack = memory = undefined;
    return this;
}
  • memory 保持以前的值,將新增到這個列表的後面的最新的值立即執行呼叫任何回撥
function fn1(val) {
    console.log(`fn1 says ` + val);
}
function fn2(val) {
    console.log(`fn2 says ` + val);
}
function fn3(val) {
    console.log(`fn3 says ` + val);
}

var cbs = $.Callbacks(`memory`);
cbs.add(fn1);
cbs.fire(`foo`); // fn1 says foo

console.log(`..........`)

cbs.add(fn2);  // 這裡在新增一個函式進入佇列的同時,就立馬執行了這個 回撥了
cbs.fire(`bar`); 
// fn2 says foo 這個東東比較特殊~
// fn1 says bar
// fn2 says bar

console.log(`..........`)
cbs.add(fn3);
cbs.fire(`aaron`);
// fn3 says bar
// fn1 says aaron 
// fn2 says aaron
// fn3 says aaron
// 需要解決的問題一個就是如何獲取上一個引數,以及add後的執行
function Callbacks(options) {
  var list = [];
  var self;
  var firingStart;
  var memory;

  function _fire(data) {
    memory = options === `memory` && data;
    firingIndex = firingStart || 0; // 
    firingStart = 0;
    firingLength = list.length;
    for (; list && firingIndex < firingLength; firingIndex++) {
      list[firingIndex](data)
    }
  }

  self = {
    add: function(fn) {
      var start = list.length;
      list.push(fn)
      // 如果引數是memory
      if (memory) {
        firingStart = start; //獲取最後一值
        _fire(memory); // 同時執行
      }
    },
    fire: function(args) {
      if (list) {
        _fire(args)
      }
    }
  }
  return self;
}
  • Unique:確保一次只能新增一個回撥(所以在列表中沒有重複的回撥)
function fn1(val) {
  console.log(`fn1 says ` + val);
}
var callbacks = $.Callbacks( "unique" );
callbacks.add( fn1 );
callbacks.add( fn1 ); // repeat addition
callbacks.add( fn1 );
callbacks.fire( "foo" );
  • stopOnFalse: 當一個回撥返回false 時中斷呼叫
function fn1(value) {
  console.log(value);
  return false;
}

function fn2(value) {
  fn1("fn2 says: " + value);
  return false;
}

var callbacks = $.Callbacks("stopOnFalse");
callbacks.add(fn1);
callbacks.fire("foo");

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

// foo
// bar

$.callback()的原始碼

jQuery.Callbacks = function( options ) {

    // Convert options from String-formatted to Object-formatted if needed
    // (we check in cache first)
    //通過字串在optionsCache尋找有沒有相應快取,如果沒有則建立一個,有則引用
    //如果是物件則通過jQuery.extend深複製後賦給options。
    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為true,則記錄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 ) {
                    // 阻止未來可能由於add所產生的回撥
                    memory = false; // To prevent further calls using add
                    break; //由於引數stopOnFalse為true,所以當有回撥函式返回值為false時退出迴圈
                }
            }
            //標記回撥結束
            firing = false;
            if ( list ) {
                if ( stack ) {
                    if ( stack.length ) {
                        //從堆疊頭部取出,遞迴fire
                        fire( stack.shift() );
                    }
                } else if ( memory ) {//否則,如果有記憶
                    list = [];
                } else {//再否則阻止回撥列表中的回撥
                    self.disable();
                }
            }
        },
        // Actual Callbacks object
        // 暴露在外的Callbacks物件,對外介面
        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傳進來的列表的每一個物件執行操作
                        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?
                    // 如果回撥列表中的回撥正在執行時,其中的一個回撥函式執行了Callbacks.add操作
                    // 上句話可以簡稱:如果在執行Callbacks.add操作的狀態為firing時
                    // 那麼需要更新firingLength值
                    if ( firing ) {
                        firingLength = list.length;
                    // With memory, if we`re not firing then
                    // we should call right away
                    } else if ( memory ) {
                        //如果options.memory為true,則將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迴圈的意義在於藉助於強大的jQuery.inArray刪除函式列表中相同的函式引用(沒有設定unique的情況)
                        // jQuery.inArray將每次返回查詢到的元素的index作為自己的第三個引數繼續進行查詢,直到函式列表的盡頭
                        // splice刪除陣列元素,修改陣列的結構
                        while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
                            list.splice( index, 1 );
                            // Handle firing indexes
                            // 在函式列表處於firing狀態時,最主要的就是維護firingLength和firgingIndex這兩個值
                            // 保證fire時函式列表中的函式能夠被正確執行(fire中的for迴圈需要這兩個值
                            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;
};

未完待續~~

相關文章