回調函數是每個前端程序員都應該知道的概念之一。回調可用於數組、計時器函數、promise、事件處理中。
本文將會解釋回調函數的概念,同時幫你區分兩種回調:同步和異步。
回調函數
首先寫一個向人打招呼的函數。
只需要創建一個接受 name 參數的函數 greet(name)。這個函數應返回打招呼的消息:
function greet(name) {
return `Hello, ${name}!`;
}
greet('Cristina'); // => 'Hello, Cristina!'
如果向很多人打招呼該怎麼辦?可以用特殊的數組方法 array.map() 可以實現:
const persons = ['Cristina', 'Ana'];
const messages = persons.map(greet);
messages; // => ['Hello, Cristina!', 'Hello, Ana!']
persons.map(greet) 獲取 persons 數組的所有元素,並分別用每個元素作為調用參數來調用 greet() 函數:`greet('Cristina'), greet('Ana')。
有意思的是 persons.map(greet) 方法可以接受 greet() 函數作為參數。這樣 greet() 就成了回調函數。
persons.map(greet) 是用另一個函數作為參數的函數,因此被稱為高階函數。
回調函數作為高階函數的參數,高階函數通過調用回調函數來執行操作。
重要的是高階函數負責調用回調,併為其提供正確的參數。
在前面的例子中,高階函數 persons.map(greet) 負責調用 greet() 函數,並分別把數組中所有的元素 'Cristina' 和 Ana ' 作為參數。
這就為識別回調提供了一條簡單的規則。如果你定義了一個函數,並將其作參數提供給另一個函數的話,那麼這就創建了一個回調。
你可以自己編寫使用回調的高階函數。下面是 array.map() 方法的等效版本:
function map(array, callback) {
const mappedArray = [];
for (const item of array) {
mappedArray.push(
callback(item) );
}
return mappedArray;
}
function greet(name) {
return `Hello, ${name}!`;
}
const persons = ['Cristina', 'Ana'];
const messages = map(persons, greet);messages; // => ['Hello, Cristina!', 'Hello, Ana!']
map(array, callback) 是一個高階函數,因為它用回調函數作為參數,然後在其主體內部調用該回調函數:callback(item)。
注意,常規函數(用關鍵字 function 定義)或箭頭函數(用粗箭頭 => 定義)同樣可以作為回調使用。
同步回調
回調的調用方式有兩種:同步和異步回調。
同步回調是“阻塞”的:高階函數直到回調函數完成後才繼續執行。
例如,調用 map() 和 greet() 函數。
function map(array, callback) {
console.log('map() starts');
const mappedArray = [];
for (const item of array) { mappedArray.push(callback(item)) }
console.log('map() completed');
return mappedArray;
}
function greet(name) {
console.log('greet() called');
return `Hello, ${name}!`;
}
const persons = ['Cristina'];
map(persons, greet);
// logs 'map() starts'
// logs 'greet() called'
// logs 'map() completed'
其中 greet() 是同步回調。
同步回調的步驟:
- 高階函數開始執行:
'map() starts' - 回調函數執行:
'greet() called' - .最後高階函數完成它自己的執行過程:
'map() completed'
同步回調的例子
許多原生 JavaScript 類型的方法都使用同步回調。
最常用的是 array 的方法,例如: array.map(callback), array.forEach(callback), array.find(callback), array.filter(callback), array.reduce(callback, init)
// Examples of synchronous callbacks on arrays
const persons = ['Ana', 'Elena'];
persons.forEach(
function callback(name) { console.log(name);
}
);
// logs 'Ana'
// logs 'Elena'
const nameStartingA = persons.find(
function callback(name) { return name[0].toLowerCase() === 'a';
}
);
nameStartingA; // => 'Ana'
const countStartingA = persons.reduce(
function callback(count, name) { const startsA = name[0].toLowerCase() === 'a';
return startsA ? count + 1 : count;
},
0
);
countStartingA; // => 1
字符串類型的 string.replace(callback) 方法也能接受同步執行的回調:
// Examples of synchronous callbacks on strings
const person = 'Cristina';
// Replace 'i' with '1'
person.replace(/./g,
function(char) { return char.toLowerCase() === 'i' ? '1' : char;
}
); // => 'Cr1st1na'
異步回調
異步回調是“非阻塞的”:高階函數無需等待回調完成即可完成其執行。高階函數可確保稍後在特定事件上執行回調。
在以下的例子中,later() 函數的執行延遲了 2 秒:
console.log('setTimeout() starts');
setTimeout(function later() {
console.log('later() called');
}, 2000);
console.log('setTimeout() completed');
// logs 'setTimeout() starts'
// logs 'setTimeout() completed'
// logs 'later() called' (after 2 seconds)
later() 是一個異步回調,因為 setTimeout(later,2000) 啓動並完成了執行,但是 later() 在 2 秒後執行。
異步調用回調的步驟:
- 高階函數開始執行:
'setTimeout()starts' - 高階函數完成其執行:
'setTimeout() completed' - 回調函數在 2 秒鐘後執行:
'later() called'
異步回調的例子
計時器函數異步調用回調:
setTimeout(function later() {
console.log('2 seconds have passed!');
}, 2000);
// After 2 seconds logs '2 seconds have passed!'
setInterval(function repeat() {
console.log('Every 2 seconds');
}, 2000);
// Each 2 seconds logs 'Every 2 seconds!'
DOM 事件偵聽器還異步調用事件處理函數(回調函數的子類型):
const myButton = document.getElementById('myButton');
myButton.addEventListener('click', function handler() {
console.log('Button clicked!');
});
// Logs 'Button clicked!' when the button is clicked
4.異步回調函數與異步函數
在函數定義之前加上特殊關鍵字 async 會創建一個異步函數:
async function fetchUserNames() {
const resp = await fetch('https://api.github.com/users?per_page=5');
const users = await resp.json();
const names = users.map(({ login }) => login);
console.log(names);
}
fetchUserNames() 是異步的,因為它以 async 為前綴。函數 await fetch('https://api.github.com/users?per_page=5') 從 GitHub 上獲取前5個用户 。然後從響應對象中提取 JSON 數據:await resp.json()。
異步函數是 promise 之上的語法糖。當遇到表達式 await <promise> (調用 fetch() 會返回一個promise)時,異步函數會暫停執行,直到 promise 被解決。
異步回調函數和異步函數是不同的兩個術語。
異步回調函數由高階函數以非阻塞方式執行。但是異步函數在等待 promise(await <promise>)解析時會暫停執行。
但是你可以把異步函數用作異步回調!
讓我們把異步函數 fetch UserNames() 設為異步回調,只需單擊按鈕即可調用:
const button = document.getElementById('fetchUsersButton');
button.addEventListener('click', fetchUserNames);
總結
回調是一個可以作為參數傳給另一個函數(高階函數)執行的函數。
回調函數有兩種:同步和異步。
同步回調是阻塞的。
異步回調是非阻塞的。
最後考考你:setTimeout(callback,0) 執行 callback 時是同步還是異步的?
本文首發微信公眾號:前端先鋒
歡迎掃描二維碼關注公眾號,每天都給你推送新鮮的前端技術文章
歡迎繼續閲讀本專欄其它高贊文章:
- 深入理解Shadow DOM v1
- 一步步教你用 WebVR 實現虛擬現實遊戲
- 13個幫你提高開發效率的現代CSS框架
- 快速上手BootstrapVue
- JavaScript引擎是如何工作的?從調用棧到Promise你需要知道的一切
- WebSocket實戰:在 Node 和 React 之間進行實時通信
- 關於 Git 的 20 個面試題
- 深入解析 Node.js 的 console.log
- Node.js 究竟是什麼?
- 30分鐘用Node.js構建一個API服務器
- Javascript的對象拷貝
- 程序員30歲前月薪達不到30K,該何去何從
- 14個最好的 JavaScript 數據可視化庫
- 8 個給前端的頂級 VS Code 擴展插件
- Node.js 多線程完全指南
- 把HTML轉成PDF的4個方案及實現
- 更多文章...