回撥函式
一、概念
回撥函式
是一個通過函式指標來呼叫執行的函式,如果你把一個函式的指標作為引數傳遞出去,那麼這個指標呼叫這個函式的時候,我們就說這是回撥函式。回撥函式不是由該函式的實現方直接呼叫,而是在特定的事件或條件發生時由另外的一方呼叫的,用於對該事件或條件進行響應。
好處:
使用回撥函式進行處理,程式碼就可以繼續進行其他任務,而無需空等。實際開發中,經常在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
物件,它的add
和fire
方法就是,其實就是基於釋出訂閱(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
物件,它的add
和fire
方法就是,其實就是基於釋出訂閱(Publish/Subscribe)
的觀察者模式的設計。
$.Callbacks
一般的開發者使用的較少,它的開發實現主要時為$.ajax
以及$.deferred
。
jQuery.Callbacks
是jquery
在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個引數。
-
-
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;
};
未完待續~~