簡要:$.Callbacks是一個生成回調管家Callback的工廠,Callback提供一系列方法來管理一個回調列表($.Callbacks的一個私有變數list),包括添加回調函數, 刪除回調函數等等...,話不多說看正文: memory的值由傳入$.Callbacks的形參對象決定,具有狀態記 ...
簡要:$.Callbacks是一個生成回調管家Callback的工廠,Callback提供一系列方法來管理一個回調列表($.Callbacks的一個私有變數list),包括添加回調函數,
刪除回調函數等等...,話不多說看正文:
var memory, // Last fire value (for non-forgettable lists) fired, // Flag to know if list was already fired //是否回調過 firing, // Flag to know if list is currently firing //回調函數列表是否正在執行中 firingStart, // First callback to fire (used internally by add and fireWith) //第一回調函數的下標,啟動回調任務的開始位置 firingLength, // End of the loop when firing //回調函數列表長度? firingIndex, // Index of currently firing callback (modified by remove if needed),正在執行回調函數的索引 list = [], // Actual callback list //回調數據源: 回調列表 stack = !options.once && [], // Stack of fire calls for repeatable lists//回調只能觸發一次的時候,stack永遠為false
memory的值由傳入$.Callbacks的形參對象決定,具有狀態記憶功能。當為null||false時,callback.add僅僅是添加方法,而當為true時,則添加之後會立即執行。
請參考如下代碼(來自: http://www.cnblogs.com/snandy/archive/2012/11/15/2770237.html)
function fn1() { console.log(1) } function fn2() { console.log(2) } var callbacks = $.Callbacks('memory'); callbacks.add(fn1); callbacks.fire(); // 必須先fire callbacks.add(fn2); // 此時會立即觸發fn2
如下是觸發回調任務的底層函數:
fire = function(data) { memory = options.memory && data //記憶模式,觸發過後,再添加新回調,也立即觸發。 fired = true firingIndex = firingStart || 0 //回調任務開始的索引賦值給將要執行函數的索引 firingStart = 0 //回調任務的觸發會將列表裡剩下的所有函數執行,因此下一次任務觸發肯定是從0開始的,這裡重置一下 firingLength = list.length firing = true //標記正在回調 //遍歷回調列表 for ( ; list && firingIndex < firingLength ; ++firingIndex ) { //如果 list[ firingIndex ] 為false,且stopOnFalse(中斷)模式 //list[firingIndex].apply(data[0], data[1]) 這是執行回調 if (list[firingIndex].apply(data[0], data[1]) === false && options.stopOnFalse) { memory = false //中斷回調執行 break } } firing = false //標記回調執行完畢 if (list) { //stack里還緩存有未執行的回調,如果回調任務只能執行一次則stack為false if (stack) stack.length && fire(stack.shift()) //執行stack里的回調 else if (memory) list.length = 0 //memory 清空回調列表 list.length = 0清空數組的技巧 else Callbacks.disable(); //其他情況如 once 禁用回調 } },
fire方法在回調執行前首先初始化回調索引和回調狀態,然後迴圈順序執行回調函數,傳遞參數為data[1],以data[0]為上下文執行,執行完後根據once是否只執行一次
,處理和回收list,memory,stack等變數。
var ca = { name:"tom", age:1 } function printName() { console.log(this.name+"-----"+arguments[0]); } function printAge() { console.log(this.age+"-----"+arguments[0]); } list.push(printName); list.push(printAge); fire([ca,'test']); 結果: tom-----test 1-----test
接下來是創建了一個Callbacks 對象以及一系列方法
//添加一個或一組到回調列表裡 add: function() { if (list) { //回調列表已存在 var start = list.length, //位置從最後一個開始 add = function(args) { //參數可以是:fn,[fn,fn],fn $.each(args, function(_, arg){ if (typeof arg === "function") { //是函數 //非unique,或者是unique,但回調列表未添加過 if (!options.unique || !Callbacks.has(arg)) list.push(arg) } //是數組/偽數組,添加,重新遍歷 else if (arg && arg.length && typeof arg !== 'string') add(arg) }) } //添加進列表 add(arguments) //如果列表正在執行中,修正長度,使得新添加的回調也可以執行, //firing:true表明fire中的迴圈執行還未結束,此時可以修改length;為false則表示迴圈執行結束了 if (firing) firingLength = list.length else if (memory) { //memory 模式下,修正開始下標,start為list.length,這裡只迴圈一次 firingStart = start fire(memory) //立即執行所有回調 } } return this }
add方法是將一系列的fn加入到回調列表中,內部的add方法用到了遞歸技巧,同時對於回調任務的執行期間做出相應處理
,如下是例子(參考鏈接:http://www.cnblogs.com/snandy/archive/2012/11/15/2770237.html)
function fn1() { console.log(1) } function fn2() { console.log(2) } var callbacks = $.Callbacks(); // 方式1 callbacks.add(fn1); // 方式2 一次添加多個回調函數 callbacks.add(fn1, fn2); // 方式3 傳數組 callbacks.add([fn1, fn2]); // 方式4 函數和數組摻和 callbacks.add(fn1, [fn2]);
remove方法會從回調列表中刪除一個或一組fn(有去重功能)
//從回調列表裡刪除一個或一組回調函數,remove(fn),remove(fn,fn) remove: function() { if (list) { //回調列表存在才可以刪除 //_作廢參數 //遍歷參數 $.each(arguments, function(_, arg){ var index //如果arg在回調列表裡 while ((index = $.inArray(arg, list, index)) > -1) { list.splice(index, 1) //執行刪除 // Handle firing indexes //回調正在執行中 if (firing) { //避免回調列表溢出 if (index <= firingLength) --firingLength //在正執行的回調函數後,遞減結尾下標 if (index <= firingIndex) --firingIndex //在正執行的回調函數前,遞減開始下標 } } }) } return this }
方法中的while迴圈是去除回調列表中重覆的函數的技巧,去除指定fn之後,如果此時回調列表正在執行回調任務,則修正回調索引(因為list中所有的回調函數的索引都改變了),這裡能傳多個fn做參數,它會迴圈刪除,如下例子(參考鏈接:http://www.cnblogs.com/snandy/archive/2012/11/15/2770237.html)
function fn1() { console.log(1) } function fn2() { console.log(2) } var callbacks = $.Callbacks(); callbacks.add(fn1, fn2); callbacks.remove(fn1); //此時fire只會觸發fn2了。 var callbacks = $.Callbacks(); callbacks.add(fn1, fn2, fn1, fn2); callbacks.remove(fn1); //此時會把add兩次的fn1都刪掉,fire時只觸發fn2兩次。換成if則只刪fn1一次
Callbacks裡面的函數封裝了fire方法,Callbacks.fire(args),回調函數將以Callbacks為this,args為參數調用,stack的作用是若回調列表處於觸髮狀態,此時將
本次要觸發的任務信息存入stack中
/** * 用上下文、參數執行列表中的所有回調函數 * @param context * @param args * @returns {*} */ fireWith: function(context, args) { // 未回調過,非鎖定、禁用時 if (list && (!fired || stack)) { args = args || [] args = [context, args.slice ? args.slice() : args] if (firing) stack.push(args) //正在回調中 ,存入static else fire(args) //否則立即回調 } return this }, /** * 用參數執行列表中的所有回調函數 * @param context * @param args * @returns {*} */ fire: function() { //執行回調 return Callbacks.fireWith(this, arguments) }
如下:(參考鏈接:http://www.cnblogs.com/snandy/archive/2012/11/15/2770237.html)
function fn() { console.log(this); // 上下文是callbacks console.log(arguments); // [3] } var callbacks = $.Callbacks(); callbacks.add(fn); callback.fire(3); //前面已經提到了,fire方法用來觸發回調函數,預設的上下文是callbacks對象, //還可以傳參給回調函數。 function fn() { console.log(this); // 上下文是person console.log(arguments); // [3] } var person = {name: 'jack'}; var callbacks = $.Callbacks(); callbacks.add(fn); callback.fireWith(person, 3); //callback.fire.call(person, 3); //其實fire內部調用的是fireWith,只是將上下文指定為this了, //而this正是$.Callbacks構造的對象。
設計思考:
add,remove方法和fire等方法內部都加入了對回調列表的狀態的判斷和相應處理,比如fireWith方法內部判斷當前回調任務是否正在進行,如果是,則將要執行的fn暫時加入到stack中。但這一設計思路對於單線程的js來說有點不太合理,如果是先執行回調列表,再fireWith,則實際過程是回調任務執行完之後再執行fireWith,這個時候回調任務已經結束了,不可能存在firing的情況。但為何jser還是要這麼設計呢?
這裡的設計是針對多線程來設計的。回調任務開始的同時,容許另一線程操作並修改回調列表內容。假想這樣一個場景:有一個網路機器人R,功能是執行所有線上客戶要求執行的一系列操作,某一時刻,客戶A上傳了一系列操作(命名為DO),當R正在執行操作時,客戶A要求此時執行DO,而R正在執行其他操作,此時,R是被鎖住的。DO則被加入到緩衝隊列中延後執行。
這裡在舉個例子說明 來自鏈接:http://www.cnblogs.com/snandy/archive/2012/11/15/2770237.html
// 觀察者模式 var observer = { hash: {}, subscribe: function(id, callback) { if (typeof id !== 'string') { return } if (!this.hash[id]) { this.hash[id] = $.Callbacks() this.hash[id].add(callback) } else { this.hash[id].add(callback) } }, publish: function(id) { if (!this.hash[id]) { return } this.hash[id].fire(id) } } // 訂閱 observer.subscribe('mailArrived', function() { alert('來信了') }) observer.subscribe('mailArrived', function() { alert('又來信了') }) observer.subscribe('mailSend', function() { alert('發信成功') }) // 發佈 setTimeout(function() { observer.publish('mailArrived') }, 5000) setTimeout(function() { observer.publish('mailSend') }, 10000)
結束語:本文素材多來自其他文章並加上了自己的理解,感謝如下兩位博客:
http://www.cnblogs.com/snandy/archive/2012/11/15/2770237.html
http://www.cnblogs.com/mominger/p/4369469.html