昨天寫到在 loadScript
的例子中加入 callback,處理非同步完成後要做的事。那麼如果想要在載入檔案後再載入其他檔案,就會在 callback 中呼叫第二次 loadScript
:
loadScript('/my/script.js', function(script) {
alert(`Cool, the ${script.src} is loaded`);
loadScript('/my/script2.js', function(script) {
alert(`Cool, the second script is loaded`);
});
});
以此類推,如果要載入很多檔案,就會變成多層的結構,開始不好閱讀。不過這裡還只算是回呼地獄的大門口,還有一個問題還沒討論,那就是要考慮出錯的情況。
如果幫 loadScript
函式加入錯誤處理,可以使用同一個 callback,成功的時候呼叫 callback(null, script)
,否則呼叫 callback(error)
:
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);
}
實際使用:
loadScript('/my/script.js', function(error, script) {
if (error) {
// 處理錯誤
} else {
// script 成功載入
}
});
這種方式是優先處理錯誤的 callback (error-first callback),它的想法是:
callback(error)
。callback(null, arg1, arg2...)
。結合上述的巢狀結構和錯誤處理,非同步的任務一多,就可以如願 (?) 進到回呼地獄:
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 {
// ...這裡繼續做所有 script 都載入後要做的事
}
});
}
});
}
});
而且可以想像一下程式碼中 ...
的地方可能包含其他要處理的邏輯,就會更複雜。
下方的例子把每個 callback 獨立拆出來避免無限巢狀,但這樣還是不太好,因為:
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 {
// ...continue after all scripts are loaded (*)
}
}
接下來就會開始寫到 promise 如何避免這些問題。
[附註] 回呼地獄 (callback hell)、波動拳、pyramid of doom 指的都是同樣的東西,感覺崩潰的事物很能激發人的想像力。