# 簡介:回調
JavaScipt 中的許多動作都是**異步**的。
比如,這個`loadScript(src)`函數:
~~~js
function loadScript(src) {
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}
~~~
這個函數的作用是加載一個新的腳本。當使用`<script src="…">`將其添加到文檔中時,瀏覽器就會對它進行加載和執行。
我們可以像這樣使用:
~~~js
// 加載并執行腳本
loadScript('/my/script.js');
~~~
函數是**異步**調用的,因為動作不是此刻(加載腳本)完成的,而是之后。
調用初始化腳本加載,然后繼續執行。當腳本正在被加載時,下面的代碼可能已經完成了執行,如果加載需要時間,那么同一時間,其他腳本可能也會被運行。
~~~js
loadScript('/my/script.js');
// 下面的代碼在加載腳本時,不會等待腳本被加載完成
// ...
~~~
現在,我們假設想在新腳本被加載完成時,被立即使用。它可能聲明了新函數,因此我們想要運行它們。
但如果我們在`loadScript(…)`調用后,立即那么做,就會導致操作失敗。
~~~js
loadScript('/my/script.js'); // 腳本含有 "function newFunction() {…}"
newFunction(); // 沒有這個函數!
~~~
很明顯,瀏覽器沒有時間去加載腳本。因此,對新函數的立即調用失敗了。`loadScript`函數并沒有提供追蹤加載完成時方法。腳本加載然后最終的運行,僅此而已。但我們希望了解腳本何時加載完成,以使用其中的新函數和新變量。
我們將`callback`函數作為第二個參數添加至`loadScript`中,函數在腳本加載時被執行:
~~~js
function loadScript(src, *!*callback*/!*) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
~~~
如果現在你想從腳本中調用新函數,我們應該在回調函數中那么寫:
~~~js
loadScript('/my/script.js', function() {
// 在腳本被加載后,回調才會被運行
newFunction(); // 現在起作用了
...
});
~~~
這是我們的想法:第二個參數是一個函數(通常是匿名的)會在動作完成后被執行。
這是一個可運行的真實腳本示例:
~~~js
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(`Cool, the ${script.src} is loaded`);
alert( _ ); // 在加載的腳本中聲明的函數
});
*/!*
~~~
這被稱為“基于回調”的異步編程風格。異步執行某些動作的函數,應該提供一個在函數完成時可以運行的`callback`參數。
我們`loadScript`中就是那么做的,但很明顯這是一般性的方法。
## [](https://github.com/javascript-tutorial/zh.javascript.info/blob/master/1-js/11-async/01-callbacks/article.md#%E5%9C%A8%E5%9B%9E%E8%B0%83%E4%B8%AD%E5%9B%9E%E8%B0%83)在回調中回調
如何順序加載兩個腳本:先是第一個,然后是第二個?
最明顯的方法是將第二個`loadScript`調用放在回調中,就像這樣:
~~~js
loadScript('/my/script.js', function(script) {
alert(`Cool, the ${script.src} is loaded, let's load one more`);
*!*
loadScript('/my/script2.js', function(script) {
alert(`Cool, the second script is loaded`);
});
*/!*
});
~~~
在外部`loadScript`完成時,內部回調就會被回調。
如果我們還想要一個腳本呢?
~~~js
loadScript('/my/script.js', function(script) {
loadScript('/my/script2.js', function(script) {
*!*
loadScript('/my/script3.js', function(script) {
// ...在所有腳本被加載后繼續操作
});
*/!*
})
});
~~~
因此,每一個動作都在回調內部。這對于新動作來說,非常好,但是其他動作卻并不友好,因此我們接下來會看到一些此方法的變體。
## [](https://github.com/javascript-tutorial/zh.javascript.info/blob/master/1-js/11-async/01-callbacks/article.md#%E5%A4%84%E7%90%86%E9%94%99%E8%AF%AF)處理錯誤
上述示例中,我們并沒有考慮錯誤因素。假如加載失敗會如何?我們的回調應該可以立即對其做出響應。
這是可以跟蹤錯誤的`loadScript`改進版:
~~~js
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);
}
~~~
成功時,調用`callback(null, script)`,否則調用`callback(error)`。
用法:
~~~js
loadScript('/my/script.js', function(error, script) {
if (error) {
// handle error
} else {
// 成功加載腳本
}
});
~~~
再一次強調,我們使用的`loadScript`方法是非常常規的。它被稱為 "error-first callback" 風格。
慣例是:
1. `callback`的第一個參數是為了錯誤發生而保留的。一旦發生錯誤,`callback(err)`就會被調用。
2. 第二個參數(如果有需要)用于成功的結果。此時`callback(null, result1, result2…)`將被調用。
因此單個`callback`函數可以同時具有報告錯誤以及傳遞返回結果的作用。
## [](https://github.com/javascript-tutorial/zh.javascript.info/blob/master/1-js/11-async/01-callbacks/article.md#%E5%9B%9E%E8%B0%83%E9%87%91%E5%AD%97%E5%A1%94)回調金字塔
從第一步可以看出,這是異步編碼的一種可行性方案。的確如此,對于一個或兩個的簡單嵌套,這樣的調用看起來非常好。
但對于一個接一個的多個異步動作,代碼就會變成這樣:
~~~js
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 {
*!*
// ...加載所有腳本后繼續 (*)
*/!*
}
});
}
})
}
});
~~~
上述代碼中:
1. 我們加載`1.js`,如果沒有發生錯誤。
2. 我們加載`2.js`,如果沒有發生錯誤。
3. 我們加載`3.js`,如果沒有發生錯誤 —— 做其他操作`(*)`。
如果嵌套變多,代碼層次就會變深,維護難度也隨之增加,尤其是如果我們有一個不是`...`的真實代碼,就會包含更多的循環,條件語句等。
這有時稱為“回調地獄”或者“回調金字塔”。
[](https://github.com/javascript-tutorial/zh.javascript.info/blob/master/1-js/11-async/01-callbacks/callback-hell.png)
嵌套調用的“金字塔”在每一個異步動作中都會向右增長。很快就會失去控制。
因此這種編碼方式并不可取。
我們可以通過為每個動作編寫一個獨立函數來解決這一問題,就像這樣:
~~~js
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 {
// ...在所有腳本被加載后繼續 (*)
}
};
~~~
看到了么?效果一樣,但是沒有深層的嵌套了,因為我們使每個動作都有一個獨立的頂層函數。
這很有效,但代碼看起來就像是一個被分裂的表格。你可能注意到了,它的可讀性非常差。在閱讀時,需要在塊之間切換。這非常不方便,尤其是不熟悉代碼的讀者,他們甚至不知道該跳轉到何處。
名為`step*`的函數都是單一使用的,他們被創建的唯一作用就是避免“回調金字塔”。沒有人會在動作鏈之外重復使用它們。因此這里的命名空間非常雜亂。
或許還有更好的方法。
幸運地是,有其他方法可以避免回調金字塔。其中一個最好的方法是使用 "promises",我們將在下一章中詳細描述。
- 內容介紹
- EcmaScript基礎
- 快速入門
- 常量與變量
- 字符串
- 函數的基本概念
- 條件判斷
- 數組
- 循環
- while循環
- for循環
- 函數基礎
- 對象
- 對象的方法
- 函數
- 變量作用域
- 箭頭函數
- 閉包
- 高階函數
- map/reduce
- filter
- sort
- Promise
- 基本對象
- Arguments 對象
- 剩余參數
- Map和Set
- Json基礎
- RegExp
- Date
- async
- callback
- promise基礎
- promise-api
- promise鏈
- async-await
- 項目實踐
- 標簽系統
- 遠程API請求
- 面向對象編程
- 創建對象
- 原型繼承
- 項目實踐
- Classes
- 構造函數
- extends
- static
- 項目實踐
- 模塊
- import
- export
- 項目實踐
- 第三方擴展庫
- immutable
- Vue快速入門
- 理解MVVM
- Vue中的MVVM模型
- Webpack+Vue快速入門
- 模板語法
- 計算屬性和偵聽器
- Class 與 Style 綁定
- 條件渲染
- 列表渲染
- 事件處理
- 表單輸入綁定
- 組件基礎
- 組件注冊
- Prop
- 自定義事件
- 插槽
- 混入
- 過濾器
- 項目實踐
- 標簽編輯
- iView
- iView快速入門
- 課程講座
- 環境配置
- 第3周 Javascript快速入門