程式碼的本質突出順序、有序這一概念,尤其在javascript——畢竟javascript是單執行緒引擎。
javascript擁有函數式程式設計的特性,而又因為javascript單執行緒引擎,我們的函數總是需要有序的執行。優秀程式碼常常把函數切割成各自的模組,然後在某一特定條件下執行,既然這些函數是有序的執行,那麼我們為什麼不寫一個統一管理的對象,來幫助我們管理這些函數——於是, Callbacks(回呼函數)誕生。
什麼是Callbacks
javascript充斥著函數編程,例如最簡單的window.onload承接的就是一個函數,悲催的是window.onload直接賦值的話只能接收一個函數,如果有好幾個函數想要在onload中執行,那我們就需要寫如下程式碼:
function a(elem) {
elem.innerHTML = '我是函數a,我要改變Element的HTML結構';
};
function b(elem) {
elem.innerHTML = '我的函數b,而我要改變Element的style';
}
window.onload = function () {
var elem = document.getElementById('test');
a(elem);
b(elem);
};
回呼函數初衷就是建立在這麼個玩意兒的上面,不再讓我們分散這些函數,而是把這些函數統一整理。可以看見,我們在window.onload中希望針對一個Element做兩件事:先改變html結構,然後改變這個html的style。兩個函數同樣是針對一個Element操作,而這兩個函數最終的執行都是有序進行的。那我們為什麼不寫一個這樣的物件來管理這些函數呢。當然, 這只是回呼函數的最基礎的存在意義,我們需要的不僅僅是這樣一個簡單的回呼函數對象,我們需要一個更強大的回呼函數。好吧,這只是一個簡單的用例,那麼我可以告訴你這個回呼函數除了一個個執行函數之外,它還可以做些什麼。
Callbacks本質就是控制函數有序的執行,Javascript是單執行緒引擎,也就說,javascript同一時間只會有一個程式碼在運作-即使是Ajax、setTimeout。 這兩個函數看起來好像都是非同步的,其實並非如此,瀏覽器在運行javascript程式碼的時候,這些程式碼都會被有序的壓入一個佇列中,當你執行Ajax的時候,瀏覽器會把Ajax壓入程式碼佇列,瀏覽器在處理javascript程式碼是從這個程式碼佇列中一個一個取程式碼執行的-Callbacks,迎合了這個單執行緒引擎。
當然,我們想要的,不僅僅是這樣一個簡單的工具物件——在jQuery原始碼中,Callbacks提供了一組函數的基本管理,為Deferred(非同步隊列)提供了基礎,同時也服務於Queue (同步隊列)。 Deferred用於抹平/扁平化金字塔程式設計(大量的回呼函數嵌套,例如Ajax中需要根據請求返回碼決定執行的程式碼); 而Queue,驅動著jQuery.animate(動畫引擎)。
那我們就來寫一個Callbacks吧。
Callbacks模型
Array(陣列):
既然我們Callbacks要承接一系列函數,必然需要有一個容器。我們可以使用一個數組,並把每個函數壓到該數組中,需要執行的時候,循環數組項執行。
工作模型:
這個Callbacks需要非常的強大,並且不僅僅是壓入函數,然後執行這麼簡單,這個Callbacks應該擁有良好的執行模型。
once:目前Callbacks物件中所有的函數只會執行一次,執行一次完之後就會被釋放掉,我們可以為使用Callbacks物件的使用者提供一個穩定有效的方案,確保函數只會執行一次,之後不再執行,穩定了這些函數的線程。
auto:自動執行模型,這是個有意思的模型,有些函數依賴上一層函數,例如函數b的執行依賴於函數a,那麼我們提供一個自動執行的模型:第一次執行這個Callbacks之後,每次加入函數到Callbacks的時候,自動執行過去添加的那些函數,並把最後一次給定的參數資料傳遞給過去的那些函數,這樣就從Callbacks中抹平了這些依賴函數之間需要反覆觸發的關係,這是個有意思的模型。
once&auto:我們可以讓它更強大,同時工作once和auto模型,即:當每次添加函數到Callbacks中的時候,過去的函數都會執行,然後,釋放掉這些過去的函數,下次繼續添加函數的時候,過去的那些函數不會再執行,因為once模型,已經把它們釋放掉了。
API:
add(function) - 添加一個(或多個)函數到Callbacks物件中:當然,如果你不添加函數只是好奇看看Callbacks,我們也將讓你繼續享受你的樂趣——我們並不會拋出異常,因為這對我們來說並不擅長。
remove(function) - 移除一個Callbacks中的一個函數:既然有了添加,那麼我們也應該提供反悔的方案,我們是多麼的平易近人,容忍著別人過去所做的一切。
has(function) - 判斷Callbacks中是否包含一個函數:哦?你竟然不確定是否包含這個函數,當初可是你丟進來的啊!你怎麼如此馬虎?不過既然你問我的話,我還是會告訴你Callbacks是否包含這個函數,我知道你很繁忙,並不能記住和確定所有的事情。
empty() - 清空Callbacks:這些函數對於你失去了意義了麼?什麼?已經執行過你就不想要了?所以你希望可以清空它?好吧,為了內存君我還是忍下你這個需求。
disable() - 廢掉一個Callbacks:為了和別人的程式碼穩定的存在,我選擇了自我犧牲-沒錯,這個方法可以廢掉Callbacks,徹底的廢掉,就如同它曾經尚未存在過一般。
disabled() - 判斷這個Callbacks是否已經被廢掉:如果你仍然不相信Callbacks是否真的自我犧牲,那麼這個方法可以讓你安心。
lock(boolean) - 鎖定這個Callbacks物件:你害怕它並不穩定,但是你又不想捨棄它,lock是個不錯的方法,它接收一個Boolean的參數,表示是否需要鎖定這個對象,當然,無參的它用於讓你確定Callbacks是否被鎖定。
fire(data) - 執行這個Callbacks中的函數:我們所做的這一切,不都是為了這一刻執行的宿命麼?參數將會成為這些需要執行的函數的參數。
fireWith(context,data) - 執行Callbacks中的函數,並且指定上下文。在fire()裡,所有的函數的Context(上下文)都是Callbacks對象,而fireWidth(),可以讓你重新定義這些要執行的函數的上下文,多麼自由的程式設計啊,Callbacks為你考慮了一切。
fired() - 判斷這個Callbacks過去是否已經執行過:我們相信,多數時候你並不知道過去做過什麼,但是我們記錄了你所做的一切,如果你過去曾經執行過這個Callbacks對象,那麼你休想否認,因為我們知道過去你是否執行了這個Callbacks。
基本模組實作
簡單的實作:
我們先來簡單的實作一個Callbacks:
(function (window, undefined) {
var Callbacks = function () {
//則以閉包保護這些私有變數
var list = [],//回呼函數清單
fired;//是否執行
//返回一個閉包的Callbakcs物件
return {
add: function (fn) {
//且被丟棄的時候,且list為undefined
if (list) {
//增加一個回呼函數
list.push(fn);
//支援鍊式回呼
}
return this;
},
fireWith: function (context, data) {
//觸發回調函數,並指定情境
if (list) {
fired = true;
for (var i = 0, len = list.length; i 而 , break; }
}
return this;
},
fire: function () {
//觸發回調函數
//且呼叫fireWith並指定情境
return this.fireWith(this, arguments);
},
empty: function () {
//以作用方式
if (list)//當這個Callbacks被丟棄的時候,Callbacks不應該可以繼續使用
list = [];
return this;
},
disable: function () {
//廢棄這個Callbacks對象,且後續的回呼函數清單已不再執行
list = undefined;
return this;
},
disabled: function () {//偵測此Callbacks是否已被廢至
//轉換與boolean返回
return !list;
},
fired: function () {//此callbacks是否執行
return !!fired;
}
};
};
//註冊到window下
window.Callbacks = Callbacks;
}(window));
然後我們來測試一下這個Callbacks:
var test = new Callbacks();
test.add(function (value) {
console.log('函數1,value是:' value);
});
test.add(function (value) {
console.log('函數2,value是:' value);
});
test.fire('這是函數1與函數2的值');
console.log('檢視函數是否執行:' test.fired());
test.disable();//廢棄這個Callbacks
console.log('檢視函數是否已被廢棄:' test.disabled());
test.add(function () {
console.log('新增第三個函數,此函數不該被執行』);
});
test.fire();
開啟瀏覽器的控制台我們可以看見運作結果正常。
once和auto(memory)實作
once:
once讓這個callbacks中的函數運行一次之後就不再運行。原理非常的簡單,上面的程式碼中,我們可以看見有一個變數list承接函數列表,所以我們只需要把過去執行過的程式碼清空即可。我們用一個全域變量,保存目前執行模型,如果是once模型,就在fireWith()裡讓這個list失效即可:
(function (window, undefined) {
var Callbacks = function (once) {
//則以閉包保護這些私有變數
var list = [],//回呼函數清單
fired;//是否執行
//返回一個閉包的Callbakcs物件
return {
//...省略部分代碼
fireWith: function (context, data) {
//觸發回調函數,並指定情境
if (list) {
fired = true;
for (var i = 0, len = list.length; i 而 , break; }
}
//若配置了once模型,且全域變數once為true,則list重設
if (once) list = undefined;
return this;
}
//...省略部分代碼
};
};
//註冊到window下
window.Callbacks = Callbacks;
}(window));
auto:
auto(memory)模型在jQuery中是以memory命名的,最初被這個命名給混淆了,仔細看了用法才確定改成auto——它的作用就是「第一次fire()之後,後續add()的函數自動執行”,以下情況可以用到:當添加一組函數到Callbacks之後,臨時又需要追加一個函數,那麼即時運行這個新追加的函數——不得不說,為了使用的便利,這個模式變得有點難以理解。實現起來就是在add()的時候判斷是否是auto模型,如果是auto模型,則執行這個函數。 但是,我們需要在第一次fire()之後才自動執行,沒有fire()過的Callbacks並不該被自動執行,而且,每次自動執行後,還需要把最後一次使用的參數傳遞給這個自動執行的函數。
或許大家會想到以下程式碼:
(function (window, undefined) {
var Callbacks = function (once, auto) {
var list = [],
fired,
lastData;//保存上一次執行的參數
return {
add: function (fn) {
if (list) {
list.push(fn);
// — 自動執行模式
//最後使用的參數傳遞過去,而其中一個被傳送了Context(上下文)
//為了不讓這裡遺失上下文,我們或許也需要聲明一個變數保存最後一次使用的Context if (auto) this.fire(lastData);
}
return this;
},
fireWith: function (context, data) {
if (list) {
lastData = data;// — 記錄最後使用的參數
fired = true;
for (var i = 0, len = list.length; i , break; }
}
if (once) list = [];
return this;
}
//部分代碼省略
};
};
//註冊到window下
window.Callbacks = Callbacks;
}(window));
但是在jQuery裡採用了更奇妙的用法,獲取jQuery作者也自豪這種用法,所以命名這個模型為memory——就是讓上面的變量auto不僅僅表示當前是auto執行模式,並且作為最後一次參數的容器,它既表示了auto,也表示了memory。 (下面的程式碼非jQuery是根據jQuery程式碼想法而寫,非原始碼):
(function (window, undefined) {
var Callbacks = function (auto) {
var list = [],
fired,
memory,//主演在這裡,即memory
coreFire = function (data) {
//真正的觸發函數方法
if (list) {
//&&且有表達式搭配
memory = auto && data;//使用最後一次的參數,若不是auto模式則不會記錄這個參數 //如果是auto模式,那麼這個auto不會是為false,且它會成為陣列
fired = true;
for (var i = 0, len = list.length; i 地 if (list[i]). , break; }
}
};
return {
add: function (fn) {
if (list) {
//增加一個回呼函數
list.push(fn);
//自動執行模式,且注意若auto模式
//memory是在coreFire()中所賦值的,而預設為false
if (memory) coreFire(auto);
;
; }
//以連結鍊式回呼
return this;
},
fireWith: function (context, data) {
if (once) list = [];
//這裡以coreFire,以參數轉換為陣列了
coreFire([context,data]);
return this;
}
/*部份代碼省略*/
};
};
window.Callbacks = Callbacks;
}(window));
我們在上一個auto實作的程式碼中看到我們遺失了Context,jQuery早在fireWith()中修復了這個bug-在fireWith()中修復參數。 jQuery把fireWith()中本來應該執行函數的邏輯給抽離出來,我們暫時將它命名為coreFire(),在原fireWith()中,將參數拼接成一個數組:第一個參數表示上下文,第二個參數表示傳遞進來的參數。然後執行coreFire()。
在add()的時候,jQuery並沒有給變數auto(memory)賦值,而是選擇在coreFire()中給auto(memory)賦值,這樣就保證了第一次fire()之後才會開啟自動執行。
依照上面所說,coreFire()接收的參數其實是數組,第一個參數是上下文,第二個參數是外面傳遞進來的參數。同時把這個陣列賦值給auto(memory),這樣,變數auto(是否自動執行模式)的定義就變成了memory(記憶最後一次傳遞的參數)。
真是一石二鳥的神思路,神想法,不得不按讚。我定義這個為auto是因為它的本身就是一個自動執行的模型,順便保存了最後一次fire()的參數,而jQuery定義為memory或許也是作者感嘆這裡的鬼斧神工吧。
至於once&auto就是把這兩個程式碼揉合在一起而已,只需要在coreFire()裡判定如果是auto模式,那麼就把list重置為一個新的數組,否則直接設定為undefined即可。
源碼
這份程式碼是自己對應jQuery手寫的一份,將一些jQuery公有的函數都寫了進來,並非程式碼片段,所以可以直接引用運行。
(function (window, undefined) {
/*
* 一個回呼函數工具對象,注意這個工作對象工作完成之後就會清空數組:
* 提供一組普通的API,但它有以下工作模型 -
* once - 單一執行模式:每次工作一次,且後續已運作
* auto - 自動執行模型:每新增一個回呼函數,自動執行現有的回呼函數中所有的回呼函數,並將本次的參數傳遞給所有的回呼函數 *
*/
//工具函數
var isIndexOf = Array.prototype.indexOf, //Es6
toString = Object.prototype.toString, //快取toString方法
toSlice = Array.prototype.slice, //快取slice方法
isFunction = (function () { //判定物件是否為Function
return "object" === typeof document.getElementById ?
isFunction = function (fn) {
//ie下DOM及BOM的辨識有問題
try {
return /^s*bfunctionb/.test("" fn);
} catch (x) {
return 一點
}
} :
isFunction = function (fn) { return toString.call(fn) === '[object Function]'; };
})(),
each = function () { //循環方式
//第一個參數表示要循環的陣列,第二個參數是每次循環執行的函數
if (arguments.length
//為什麼slice無效? ?
var list = toSlice.call(arguments[0]),
fn = arguments[1],
item;
while ((item = list.shift())) {//沒有直接判定length,加速
// 為什麼這裡用call可以,而apply就不行?
//搞定 - apply的第二個參數必須是一個array物件(沒有驗證array-like是否可以,而call沒有這個要求)
//apply是這樣描述的:如果 argArray(第二個參數) 不是一個有效的陣列或不是 arguments 對象,那麼將會導致一個 TypeError。
fn.call(window, item);
}
},
inArray = function () { //偵測已處對中是否包含某項,並傳回此項索引
//預編譯
return isIndexOf ? function (array, elem, i) {
if (array)
return isIndexOf.call(array, elem, i);
return -1;
} : function (elem, array, i) {
var len;
if (array) {
len = array.length;
我=我?我
for (; i
if (i in array && array[i] === elem) {
請與我所求;
}
}
}
回-1;
}
}();
var Callbacks = function (option) {
option = toString.call(option) === '[object Object]' ? option : {};
//使用閉包,因為每個新建的callbacks都有自己的狀態
var list = [], //回呼清單
_list = [], //若鎖定此callbacks對象,且清除於list,且將原list置入_list
fired, //是否有執行
firingStart, //目前回呼函數清單執行的函數索引(起點)
firingLength, //回呼函數的陣列長度
auto, //標誌是否自動執行,若需要自動執行,則auto記憶著最後一次回呼的參數(最後一次fire的參數),這是一個很詭異的且奇葩的用法
//這個變數用法很詭異和犀利,既包含了是否指定執行的標誌,又記錄了資料
//這個auto配合once簡直就是喪心病狂:【第一次】執行了fire後才會自動執行,配合once可以做到:一次執行,後面不再追加和執行代碼,保證了一組回調數據的穩定和安全
stack = !option.once && [], //一個callbacks棧,如果目前正在執行回呼數組,而在執行中又新添了回調函數,那麼把新的回呼函數,那麼新的回呼函數都會壓入該堆疊
firing = false, //callbacks是否正在運作/執行
//觸發回呼函數
fire = function (data) {
//注意到此data是數組,如果已設定了auto模式,那麼auto永遠不會為false,因為auto會是數組
auto = option.auto && data; //在這裡,如果配置要求記憶最後的參數,則記憶這個參數(非常犀利的用法,直接取了資料)
fired = true;
firingIndex = firingStart || 0;
firingStart = 0;//清除firingStart(不清空下次執行有問題囉)
firingLength = list.length; //快取list長度,且外部可存取
firing = true; //正在執行回呼函數
for (; firingIndex
if (list[firingIndex].apply(data[0],data[1]) === false) {
為
//我們要先封鎖那段程式碼,因此設定auto為false
auto = false;
break;
}//當函數回傳false,且終止執行後續佇列
}
firing = false; //標誌狀態已執行完畢回呼函數[stack(堆疊)裡面的函數尚未執行]
//如果這個堆疊在沒有配置once的情況下肯定是[],所以一定存在
//這裡主要作用是,且沒有設定once,則攔截下方的程式碼,如果配置了once,執行完碼已清除資料
if (stack) {
fire(stack.shift()); //從堆疊頭部取出,並遞歸fire()方法
}
else if (auto) //程式碼走到這裡,證明已配置了option.once(只執行一次),於是清除list
list = [];
else //證明「在這個終極」中設定了一個終極符號
self.disable();
};
var self = {
add: function () {//增加一個回呼函數
if (list) {
var start = list.length;
(function addCallback(args) {
each(args, function (item) {
if (isFunction(item)) {//是函數,以壓入回呼清單
, list.push(item);
;
//注意且typeof 與Object.prototype.toString相同的
會
addCallback(item)且;
}
});
})(arguments);
}
if (firing)//如果目前正有回呼函數在執行,那麼需要更新目前回呼函數清單的length,否則這個新壓入的回呼函數就會被掠過。
firingLength = list.length;
else if (auto) {//若目前未執行回呼函數,且要求自動執行
//注意這裡有使用於firingStart賦值,而上面fire方法中正在使用的是firingIndex,這裡不會影響到上面代碼的執行線路
firingStart = start;
//執行我們新加入的夥伴
fire(auto);
}
return this;
},
fire: function () {//觸發回呼函數
self.fireWith(this, arguments);
return this;
},
fireWith: function (context, args) {//觸發回呼函數,並指定情境
//如果已設定了once,且stack將為undefined,而once需要保證只執行一次,所以一旦執行過一次,這裡的程式碼不會再執行
if (list && (!fired || stack)) {
//修正參數
//以0
//而參數清單索引為2
//轉換為陣列存取是因為物件表示更加的消耗資源,在頂層的fire()代碼中有auto[記憶參數,自動執行]這個功能,如果採用物件則開銷了更大的記憶體
args = [context,
args ?
args.slice && args.slice()
|| toSlice.call(args):
[]
];
fire(args);
}
return this;
},
remove: function () {//移除一個回呼函數
if (list) {
each(arguments, function (item) {
var index;
//且有多項,且在循環中表示檢索的範圍,且先前擷取的過的可不可擷取
while ((index = inArray(item, list, index)) > -1) {
list.splice(index, 1);
if (firing) {
的
地
地
}
}
});
}
return this;
},
has: function (fn) {//是否包含一個回呼函數
return fn ? inArray(fn, list) > -1 : list && list.length;
},
empty: function () {//清除此callbacks物件
list = [];
firingLength = 0;
return this;
},
disable: function () {//廢掉此callbacks對象,後續的回呼函數清單不再執行
list = stack = auto = undefined;
return this;
},
disabled: function () {//是否已被廢至
 

C 和JavaScript通過WebAssembly實現互操作性。 1)C 代碼編譯成WebAssembly模塊,引入到JavaScript環境中,增強計算能力。 2)在遊戲開發中,C 處理物理引擎和圖形渲染,JavaScript負責遊戲邏輯和用戶界面。

JavaScript在網站、移動應用、桌面應用和服務器端編程中均有廣泛應用。 1)在網站開發中,JavaScript與HTML、CSS一起操作DOM,實現動態效果,並支持如jQuery、React等框架。 2)通過ReactNative和Ionic,JavaScript用於開發跨平台移動應用。 3)Electron框架使JavaScript能構建桌面應用。 4)Node.js讓JavaScript在服務器端運行,支持高並發請求。

Python更適合數據科學和自動化,JavaScript更適合前端和全棧開發。 1.Python在數據科學和機器學習中表現出色,使用NumPy、Pandas等庫進行數據處理和建模。 2.Python在自動化和腳本編寫方面簡潔高效。 3.JavaScript在前端開發中不可或缺,用於構建動態網頁和單頁面應用。 4.JavaScript通過Node.js在後端開發中發揮作用,支持全棧開發。

C和C 在JavaScript引擎中扮演了至关重要的角色,主要用于实现解释器和JIT编译器。1)C 用于解析JavaScript源码并生成抽象语法树。2)C 负责生成和执行字节码。3)C 实现JIT编译器,在运行时优化和编译热点代码,显著提高JavaScript的执行效率。

JavaScript在現實世界中的應用包括前端和後端開發。 1)通過構建TODO列表應用展示前端應用,涉及DOM操作和事件處理。 2)通過Node.js和Express構建RESTfulAPI展示後端應用。

JavaScript在Web開發中的主要用途包括客戶端交互、表單驗證和異步通信。 1)通過DOM操作實現動態內容更新和用戶交互;2)在用戶提交數據前進行客戶端驗證,提高用戶體驗;3)通過AJAX技術實現與服務器的無刷新通信。

理解JavaScript引擎內部工作原理對開發者重要,因為它能幫助編寫更高效的代碼並理解性能瓶頸和優化策略。 1)引擎的工作流程包括解析、編譯和執行三個階段;2)執行過程中,引擎會進行動態優化,如內聯緩存和隱藏類;3)最佳實踐包括避免全局變量、優化循環、使用const和let,以及避免過度使用閉包。

Python更適合初學者,學習曲線平緩,語法簡潔;JavaScript適合前端開發,學習曲線較陡,語法靈活。 1.Python語法直觀,適用於數據科學和後端開發。 2.JavaScript靈活,廣泛用於前端和服務器端編程。


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

Safe Exam Browser
Safe Exam Browser是一個安全的瀏覽器環境,安全地進行線上考試。該軟體將任何電腦變成一個安全的工作站。它控制對任何實用工具的訪問,並防止學生使用未經授權的資源。

Atom編輯器mac版下載
最受歡迎的的開源編輯器

EditPlus 中文破解版
體積小,語法高亮,不支援程式碼提示功能

SecLists
SecLists是最終安全測試人員的伙伴。它是一個包含各種類型清單的集合,這些清單在安全評估過程中經常使用,而且都在一個地方。 SecLists透過方便地提供安全測試人員可能需要的所有列表,幫助提高安全測試的效率和生產力。清單類型包括使用者名稱、密碼、URL、模糊測試有效載荷、敏感資料模式、Web shell等等。測試人員只需將此儲存庫拉到新的測試機上,他就可以存取所需的每種類型的清單。