jQuery原始碼剖析(三) - Callbacks 原理分析

極客James發表於2019-07-21

jQuery 原始碼解析程式碼及更多學習乾貨: 猛戳GitHub

本篇程式碼為

my-jQuery 1.0.2.js

my-jQuery 1.0.3.js

建議下載原始碼然後據文章思路學習,最好自己邊思考邊多敲幾遍。

一、Callbacks基本概念

1.$.callbacks用於管理函式佇列。
2.通過add()新增處理函式到佇列中,通過fire去執行這些函式。
3.$.callbacks是在jQuery內部使用的,如為.ajax,$.Deffed等元件提供基礎功能函式。它也可以在類似功能的一些元件中,如自己開發的外掛

二、Callbacks API

  • 1.$.callbacks,獲取例項
  • 2.add()向內部佇列新增函式
  • 3.fire()依次找到並執行佇列裡的函式
var cb = $.callbacks();
cb.add(function(){
console.log('add one');
});
cb.add(function(){
console.log('add two');
});
cb.add(function(){
console.log('add three');
});
cb.fire() 
//依次輸出
add one 
add two 
add three
複製程式碼

三、Callbacks引數的特定功能

callbacks通過字串引數的形式,支援四種特定的功能

  • 1.once
    • 函式佇列只執行一次
 // once關鍵字
var cbOne = $.Callbacks('once');
cbOne.add(function(){
    console.log("this is cbOne1");
});
cbOne.add(function(){
    console.log("this is a cbOne2");
});
// 只輸出執行一次,後面呼叫都不生效
cbOne.fire();
cbOne.fire();
執行結果:
this is cbOne1
this is a cbOne2
複製程式碼
  • 2.unique
    • 往內部佇列新增的函式保持唯一,不能重複新增
 // unique
var cbUnique = $.Callbacks('unique');
function demo(){
    console.log("this is a cbUnique");
}
cbUnique.add(demo,demo);
cbUnique.fire();
// 輸出了一次
this is a cbUnique
複製程式碼
  • 3.stopOnFalse
    • 內部佇列裡的函式是依次執行的,當某個函式的返回值是false時,停止繼續執行剩下的函式。
// stopOnFalse 關鍵字
// 不加關鍵字的情況
var cbDemo = $.Callbacks();
cbDemo.add(function(){
    console.log("this is cbDemo 1");
    return false;
    
},function(){
    console.log("this is cbDemo 2");
})
cbDemo.fire();
輸出:
this is cbDemo 1
this is cbDemo 2

// 加關鍵字的情況
var cbStopOnFalse = $.Callbacks('stopOnFalse');
cbStopOnFalse.add(function(){
    console.log("this is a cbStopOnFalse 1");
    return false;
},function(){
    console.log("this is a cbStopOnFalse 2");
});
cbStopOnFalse.fire(); 
輸出:
this is a cbStopOnFalse 1
複製程式碼
  • 4.memory
    • 當引數佇列fire一次過後,內部會記錄當前fire的引數。當下次呼叫add的時候,會把記錄的引數傳遞給新新增的函式並立即執行這個新新增的函式。
// 引數 memory
// 不加引數的情況
var cbNoMemory = $.Callbacks();
cbNoMemory.add(function(){
    console.log("this is a cbNoMemory 1");
});
cbNoMemory.fire(); 
輸出:
this is a cbNoMemory 1
cbNoMemory.add(function (){
    console.log("this is a cbNoMemory 2");
});

// 新增引數的情況
var cbMemory = $.Callbacks('memory');
cbMemory.add(function(){
    console.log("this is a cbMemory 1");
});
cbMemory.fire(); 
輸出:
this is a cbMemory 1
this is a cbMemory 2

cbMemory.add(function(){
    console.log("this is a cbMemory 2");
})
複製程式碼

四、從事件函式了解Callbacks

1.事件通常與函式配合使用,這樣就可以通過發生的事件來驅動函式的執行.

原則:一個事件對應一個事件函式 在一個事件對應多個函式的情況下,後者會覆蓋掉前者。

問題: 那麼我們能否有一種方案來改變一對一的事件模型呢?

解決方案: 把事件放到一個陣列裡,然後通過遍歷陣列依次執行的方式來達到一對多的事件模型。

// 一對多事件模型
function one(){
    console.log("one");
    
};
function two(){
    console.log("two");
    
};
function three(){
    console.log("three");
    
};
function four(){
    console.log("four");
    
};
var clickCallBack = [one,two,three,four];

// 在body中定義一個button <button id="btn">按鈕</button>

$("#btn").click(function(){
   var _this = this;
   clickCallBack.forEach(function(fn){
       fn.call(_this);
   })
});
// 輸出結果:
one
three
three
four
複製程式碼

2.Callbacks 不僅僅是一個陣列,可以把它看成一個容器。

五、開始剖析

上面我們已經通過jQuery來呼叫Callbacks的API並輸出了內容,根據兩個方法,add(),fire()及四個引數,"once","unique","menory","stopOnfalse",的相關特性我們開始反推實現過程.

首先是add()方法:將穿過來的options先把他們轉為真陣列,然後將陣列遍歷出來挑選出型別為"Function"的資料,將資料新增到一個空陣列中,等待執行。
僅是程式碼片段,完整程式碼下載地址: 原始碼下載

核心程式碼片段:

add:function(){
    // Array.prototype.slice.call(arguments 偽陣列轉真陣列
    var args = Array.prototype.slice.call(arguments);
    start = list.length;
    // 遍歷args 找出裡面的Function
    args.forEach(function(fn){
        // 檢索fn是是否是Function
        if (toString.call(fn) === "[object Function]") {
            // unique 不存在 且fn在list中 那麼可以把fn新增到隊裡中
                list.push(fn);
            }
        }
    });
複製程式碼

fire()方法:fire其實就是把新增到佇列中的方法依次按規則輸出執行,需要一箇中介軟體fireWith提供上下文。
核心程式碼:

var fire = function(data){
    index = 0;
    length = list.length;
    // 遍歷迴圈list
    for(; index < length; index++){
        // 通過遍歷查詢list[index]的值為false 且options有stopOnfalse這個引數時遍歷終止返回
        if (list[index].apply(data[0],data[1]) == false){
            break;
        }
    }
}
複製程式碼
// 定義一個上下文繫結函式
fileWith:function(context,arguments){
    var args = [context,arguments];
        fire(args);
}
複製程式碼
fire:function(){
    self.fileWith(this,arguments);
},
複製程式碼

到此以上程式碼可以實現 add() 方法和fire() 方法,其次我們在考慮四種引數的情況,首先我們先考慮 stopOnfalse 的情況.

stopOnfalse這個引數生效的階段是在呼叫fire()方法後執行 add() 新增的佇列函式中是否有返回false的情況,所以首先我們應該想到在fire()這個方法裡做文章.

思路:直接在遍歷方法的時候來判定optionss是否有 stopOnfalse 引數如果有立馬退出.

核心程式碼:

 var fire = function(data){
    // memory
    memory = options.memory && data;
    // 為了防止memory再次呼叫一次定義了starts
    index = 0;
    length = list.length;
    // 遍歷迴圈list
    for(; index < length; index++){
        // 通過遍歷查詢list[index]的值為false 且options有stopOnfalse這個引數時遍歷終止返回
        if (list[index].apply(data[0],data[1]) == false && options.stopOnfalse){
            break;
        }
    }
複製程式碼

once 引數生效的情況是,當once存在執行第一次完成後,如果還有fire()方法,那麼就直接退出不執行.

思路:首先明白受影響階段是fire(), 定義一個引數來記錄第一次執行fire()的方法,然後在呼叫執行fire()這個方法判斷是否傳入有 once引數如果有,那麼就不會再去執行fire()方法.

核心程式碼:

var fire = function(data){
index = 0;
length = list.length;
startAdd = true;// 用來記錄fire()方式是否執行 便於"once"方法操作
// 遍歷迴圈list
for(; index < length; index++){
    // 通過遍歷查詢list[index]的值為false 且options有stopOnfalse這個引數時遍歷終止返回
    if (list[index].apply(data[0],data[1]) == false && options.stopOnfalse){
        break;
    }
 }
}
複製程式碼
// 定義一個上下文繫結函式
fileWith:function(context,arguments){
    var args = [context,arguments];
    // 非fire做限制呼叫
    if(!options.once || !startAdd) {
        fire(args);
    }
},
複製程式碼

memory這個引數生效的情況是,如果執行 fire() 方法後,還存在 add() 的方法,那麼後面的 add() 方法依然有效。

思路: 首先要搞明白memory在哪個階段會受影響,在add()階段和fire()階段都有影響,add()階段要記錄傳入的options是否有memory這個引數,其次在執行fire()的階段,主要是要記錄住它的index值。

核心程式碼:

var fire = function(data){
    // memory
    memory = options.memory && data;
    // 為了防止memory再次呼叫一次定義了memoryStarts
    index = memoryStarts || 0;
    start = 0;
    length = list.length;
    startAdd = true; // 用來記錄fire()方式是否執行 便於"once"方法操作
    // 遍歷迴圈list
    for(; index < length; index++){
        // 通過遍歷查詢list[index]的值為false 且options有stopOnfalse這個引數時遍歷終止返回
        if (list[index].apply(data[0],data[1]) == false && options.stopOnfalse){
            break;
        }
}
}
複製程式碼
// memory 
    if (memory) {
        memoryStarts = start;
        fire(memory);
    }
複製程式碼

最難的是 unique 這個引數.

unique 這個引數生效的情況是,同一個方法被add()多次,僅執行一次改方法。

思路: unique影響階段是add()時候,所有我們在這裡做攔截操作是最好的,因此我們在add()的時候做判斷如果存在 unique 這個引數,那麼我們就不讓同樣的這個方法push到隊裡中,沒有新增到佇列,那麼我們就不會再次執行這個方法啦.

兩種方法:
(1).通過陣列的[].indexOf.call()來檢視是否存在於陣列中,不存在返回-1.
(2).可以用ES6的set進行過濾重複值
我們採用方法一來完成此操作。
核心程式碼:

// 新增 方法
add:function(){
    // Array.prototype.slice.call(arguments 偽陣列轉真陣列
    var args = Array.prototype.slice.call(arguments);
    start = list.length;
    // 遍歷args 找出裡面的Function
    args.forEach(function(fn){
        // 檢索fn是是否是Function
        if (toString.call(fn) === "[object Function]") {
            // unique 不存在 且fn在list中 那麼可以把fn新增到隊裡中
            // 處理 unique 引數
            if(!options.unique || !self.has(fn,list)) {
                list.push(fn);
            }
        }
    });
複製程式碼
has:function(fn,array){
    return arr = jQuery.inArray(fn,array) > -1;
}
複製程式碼
jQuery.inArray = function (elem,arr){
    return arr == null?-1:[].indexOf.call(arr,elem);
}
複製程式碼

至此,大功告成!! 完成了Callbacks()實現原理剖析,你是學到了呢?

其他

jQuery 原始碼剖析 系列目錄地址:猛戳GitHub

jQuery 原始碼剖析 系列預計寫十篇左右,旨在加深對原生JavaScript 部分知識點的理解和深入,重點講解 jQuery核心功能函式、選擇器、Callback 原理、延時物件原理、事件繫結、jQuery體系結構、委託設計模式、dom操作、動畫佇列等。 如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star⭐️,對作者也是一種鼓勵。

關注公眾號回覆:學習 領取前端最新最全學習資料,也可以進群和大佬一起學習交流

jQuery原始碼剖析(三) - Callbacks 原理分析

相關文章