W3Cschool
恭喜您成為首批注冊用戶
獲得88經(jīng)驗值獎勵
我們在這里的示例中使用了瀏覽器方法
為了演示回調(diào)、promise 和其他抽象概念的使用,我們將使用一些瀏覽器方法:具體地說,是加載腳本和執(zhí)行簡單的文檔操作的方法。
如果你不熟悉這些方法,并且對它們在這些示例中的用法感到疑惑,那么你可能需要閱讀本教程 下一部分 中的幾章。
但是,我們會盡全力使講解變得更加清晰。在這兒不會有瀏覽器方面的真正復(fù)雜的東西。
JavaScript 主機(host)環(huán)境提供了許多函數(shù),這些函數(shù)允許我們計劃 異步 行為(action)。換句話說,我們現(xiàn)在開始執(zhí)行的行為,但它們會在稍后完成。
例如,setTimeout
函數(shù)就是一個這樣的函數(shù)。
這兒有一些實際中的異步行為的示例,例如加載腳本和模塊(我們將在后面的章節(jié)中介紹)。
讓我們看一下函數(shù) loadScript(src)
,該函數(shù)使用給定的 src
加載腳本:
function loadScript(src) {
// 創(chuàng)建一個 <script> 標簽,并將其附加到頁面
// 這將使得具有給定 src 的腳本開始加載,并在加載完成后運行
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}
它將一個新的、帶有給定 src
的、動態(tài)創(chuàng)建的標簽 <script src="…">
插入到文檔中。瀏覽器將自動開始加載它,并在加載完成后執(zhí)行它。
我們可以像這樣使用這個函數(shù):
// 在給定路徑下加載并執(zhí)行腳本
loadScript('/my/script.js');
腳本是“異步”調(diào)用的,因為它從現(xiàn)在開始加載,但是在這個加載函數(shù)執(zhí)行完成后才運行。
如果在 loadScript(…)
下面有任何其他代碼,它們不會等到腳本加載完成才執(zhí)行。
loadScript('/my/script.js');
// loadScript 下面的代碼
// 不會等到腳本加載完成才執(zhí)行
// ...
假設(shè)我們需要在新腳本加載后立即使用它。它聲明了新函數(shù),我們想運行它們。
但如果我們在 loadScript(…)
調(diào)用后立即執(zhí)行此操作,這將不會有效。
loadScript('/my/script.js'); // 這個腳本有 "function newFunction() {…}"
newFunction(); // 沒有這個函數(shù)!
自然情況下,瀏覽器可能沒有時間加載腳本。到目前為止,loadScript
函數(shù)并沒有提供跟蹤加載完成的方法。腳本加載并最終運行,僅此而已。但我們希望了解腳本何時加載完成,以使用其中的新函數(shù)和變量。
讓我們添加一個 callback
函數(shù)作為 loadScript
的第二個參數(shù),該函數(shù)應(yīng)在腳本加載完成時執(zhí)行:
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
onload
事件在 資源加載:onload,onerror 一文中有描述,它通常會在腳本加載和執(zhí)行完成后執(zhí)行一個函數(shù)。
現(xiàn)在,如果我們想調(diào)用該腳本中的新函數(shù),我們應(yīng)該將其寫在回調(diào)函數(shù)中:
loadScript('/my/script.js', function() {
// 在腳本加載完成后,回調(diào)函數(shù)才會執(zhí)行
newFunction(); // 現(xiàn)在它工作了
...
});
這是我們的想法:第二個參數(shù)是一個函數(shù)(通常是匿名函數(shù)),該函數(shù)會在行為(action)完成時運行。
這是一個帶有真實腳本的可運行的示例:
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
alert(`酷,腳本 ${script.src} 加載完成`);
alert( _ ); // _ 是所加載的腳本中聲明的一個函數(shù)
});
這被稱為“基于回調(diào)”的異步編程風格。異步執(zhí)行某項功能的函數(shù)應(yīng)該提供一個 callback
參數(shù)用于在相應(yīng)事件完成時調(diào)用。(譯注:上面這個例子中的相應(yīng)事件是指腳本加載)
這里我們在 loadScript
中就是這么做的,但當然這是一種通用方法。
我們?nèi)绾我来渭虞d兩個腳本:第一個,然后是第二個?
自然的解決方案是將第二個 loadScript
調(diào)用放入回調(diào)中,如下所示:
loadScript('/my/script.js', function(script) {
alert(`酷,腳本 ${script.src} 加載完成,讓我們繼續(xù)加載另一個吧`);
loadScript('/my/script2.js', function(script) {
alert(`酷,第二個腳本加載完成`);
});
});
在外部 loadScript
執(zhí)行完成時,回調(diào)就會發(fā)起內(nèi)部的 loadScript
。
如果我們還想要一個腳本呢?
loadScript('/my/script.js', function(script) {
loadScript('/my/script2.js', function(script) {
loadScript('/my/script3.js', function(script) {
// ...加載完所有腳本后繼續(xù)
});
});
});
因此,每一個新行為(action)都在回調(diào)內(nèi)部。這對于幾個行為來說還好,但對于許多行為來說就不好了,所以我們很快就會看到其他變體。
在上述示例中,我們并沒有考慮出現(xiàn) error 的情況。如果腳本加載失敗怎么辦?我們的回調(diào)應(yīng)該能夠?qū)Υ俗鞒龇磻?yīng)。
這是 loadScript
的改進版本,可以跟蹤加載錯誤:
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));
document.head.append(script);
}
加載成功時,它會調(diào)用 callback(null, script)
,否則調(diào)用 callback(error)
。
用法:
loadScript('/my/script.js', function(error, script) {
if (error) {
// 處理 error
} else {
// 腳本加載成功
}
});
再次強調(diào),我們在 loadScript
中所使用的方案其實很普遍。它被稱為“Error 優(yōu)先回調(diào)(error-first callback)”風格。
約定是:
callback
? 的第一個參數(shù)是為 error 而保留的。一旦出現(xiàn) error,?callback(err)
? 就會被調(diào)用。callback(null, result1, result2…)
? 就會被調(diào)用。因此,單一的 callback
函數(shù)可以同時具有報告 error 和傳遞返回結(jié)果的作用。
乍一看,它像是一種可行的異步編程方式。的確如此,對于一個或兩個嵌套的調(diào)用看起來還不錯。
但對于一個接一個的多個異步行為,代碼將會變成這樣:
loadScript('1.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...加載完所有腳本后繼續(xù) (*)
}
});
}
});
}
});
在上面這段代碼中:
1.js
?,如果沒有發(fā)生錯誤。2.js
?,如果沒有發(fā)生錯誤……3.js
?,如果沒有發(fā)生錯誤 —— 做其他操作 ?(*)
?。隨著調(diào)用嵌套的增加,代碼層次變得更深,維護難度也隨之增加,尤其是我們使用的是可能包含了很多循環(huán)和條件語句的真實代碼,而不是例子中的 ...
。
有時這些被稱為“回調(diào)地獄”或“厄運金字塔”。
嵌套調(diào)用的“金字塔”隨著每個異步行為會向右增長。很快它就失控了。
所以這種編碼方式不是很好。
我們可以通過使每個行為都成為一個獨立的函數(shù)來嘗試減輕這種問題,如下所示:
loadScript('1.js', step1);
function step1(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', step2);
}
}
function step2(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', step3);
}
}
function step3(error, script) {
if (error) {
handleError(error);
} else {
// ...加載完所有腳本后繼續(xù) (*)
}
}
看到了嗎?它的作用相同,但是沒有深層的嵌套了,因為我們將每個行為都編寫成了一個獨立的頂層函數(shù)。
它可以工作,但是代碼看起來就像是一個被撕裂的表格。你可能已經(jīng)注意到了,它的可讀性很差,在閱讀時你需要在各個代碼塊之間跳轉(zhuǎn)。這很不方便,特別是如果讀者對代碼不熟悉,他們甚至不知道應(yīng)該跳轉(zhuǎn)到什么地方。
此外,名為 ?step*
? 的函數(shù)都是一次性使用的,創(chuàng)建它們就是為了避免“厄運金字塔”。沒有人會在行為鏈之外重用它們。因此,這里的命名空間有點混亂。
我們希望還有更好的方法。
幸運的是,有其他方法可以避免此類金字塔。最好的方法之一就是 “promise”,我們將在下一章中介紹它。
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網(wǎng)安備35020302033924號
違法和不良信息舉報電話:173-0602-2364|舉報郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: