回撥函式
小明在奶茶店點了奶茶,店員開始製作奶茶,此時“製作奶茶”與“小明等待奶茶”是一個同時進行的不同的兩個事件(任務),那麼,小明獲取店員製作成功的奶茶是從“製作奶茶”這一事件獲取的結果,所以小明才能夠完成“購買奶茶”這一事件。如果,小明在“購買奶茶”這一事件中,不想一直等待而是想去做一些其他的事情,比如購買冰淇淋。
現在,我們將這一案例抽取為一個個事件,用 JavaScript 函式體現出來:
// 小明購買奶茶事件
function buyTea() {
console.log('購買奶茶...')
}
// 店員製作奶茶事件
function makeTea() {
console.log('製作奶茶...')
}
// 小明的另一個事件
function buyIcecream() {
console.log('購買冰淇淋...')
}
目前,這些事件屬於同步任務(事件),它們被主執行緒由上到下依次執行,無法在同一時間內執行多個事件。你會發現,這些事件之間是各自獨立的。如何將它們有機地、有序地結合在一起是一個問題。
因此,在 JavaScript 中,有一種解決方式叫做非同步任務(事件),利用回撥函式將這些獨立的事件結合在一起。在 JavaScript 中,函式作為第一等公民的存在,可以將函式作為物件傳遞給方法作為實參進行呼叫,即回撥函式。
請轉至“[JS]函式作為值”一文,瞭解什麼函式是如何作為值,並且可以作為函式實參進行傳遞以及呼叫的。
function buyThing(money = 0, callback) {
let isPay = false
if (money >= 5) isPay = true
callback(isPay)
}
buyThing(10, function (isPay) {
if (isPay) {
console.log('購買奶茶...')
setTimeout(() => {
console.log('奶茶製作完成!!!')
buyThing(20, function (isPay) {
if (isPay) {
console.log('購買冰淇淋...')
setTimeout(() => {
console.log('冰淇淋製作完成!!!')
buyThing(30, function(isPay) {
// ...做很多事件,多次呼叫buyThing函式
})
}, 2000)
} else {
console.log('未支付金額...')
}
})
}, 1000)
} else {
console.log('未支付金額...')
}
})
回撥函式的確將這些獨立事件有機地結合在一起了,但是隨之而來的就是回撥地獄。
現在,我們明白了回撥函式的好處。再列舉一個例子,深入瞭解回撥函式的好處在哪。若實現一個簡單的計算器,其功能有加、減、乘、除等運算,通常情況下會想到一個函式獲得兩個引數,並將運算型別作為字串傳遞給函式引數以實現不同需求。
function calculate(x, y, type) {
if (type == 'minus') {
return x - y
} else if (type == 'add') {
return x + y
} ......
}
let result = calculate(10, 20, 'minus') // -10
上述程式碼,存在一個明顯的問題。如果在減法中做其他的限制條件(或增加原始碼的功能),它會影響到整個 calculate 函式本身。再者,如果我擴充套件 calculate 函式功能,它也會影響到函式本身。對於這種情況,我們寄希望於回撥函式,通過它來解決這個問題。
// calculate本體做一些基本的判斷(限制)
function calculate(x, y, callback) {
if (x < 0 || y < 0) {
throw new Error(`Numbers must not be negative!`)
}
if (typeof x !== 'number' || typeof y !== 'number') {
throw new Error(`Args must be number type!`)
}
return callback(x, y, 'not problem!!!') // 向外提供更多細節
}
// 沒有做任何附加限制
calculate(10, 20, function (x, y, more) {
console.log(more) // 'not problem!!!'
return x * y
})
// 做了一些附加限制
calculate(5, 5, function (x, y, more) {
console.log(more) // 'not problem!!!'
if (x + y <= 10) {
throw new Error(
'The sum of the two numbers must be greater than 10'
)
}
return x * y
})
現在,呼叫 calculate 函式時,可以在回撥函式中做一些附加限制條件,它不會影響到 calculate 這個函式本體。並且,回撥函式可以從 calculate 函式本體中獲取更多的細節(資訊),通過這些資訊我們又能做出更多的操作。
let points = [40, 100 ,10, 5, 25]
points.sort(function (a, b) => {
return a - b
}) // [5, 10, 25, 40, 100]
// ...另一種比較方式...
points.sort(function (a, b) => {
if (a < b) {
return -1
}
if (a > b) {
return 1
}
return 0
}) // [5, 10, 25, 40, 100]
在回撥函式中不同的排序方式可以決定最後的結果。回撥函式使得程式更加靈活。
回撥地獄
在上面的“小明買奶茶”案例中,回撥內部再巢狀回撥,其程式碼形狀上看著像180°旋轉之後的金字塔,這種層層巢狀就是回撥地獄。
因此,Promise 可以解決回撥地獄的問題。Promise 是一個物件,用於表示一個非同步操作的最終完成(或失敗)及其結果值。
利用 Promise 解決“小明買奶茶”回撥地獄:
function buyThing(money, timeout) {
const promise = new Promise((resolve, reject) => {
console.log('事件正在進行中...')
setTimeout(() => {
if (money >= 5) {
console.log(`支付金額:${money}`)
resolve('success to pay!')
} else {
reject('unsuccess to pay!')
}
}, timeout)
})
return promise
}
buyThing(10, 1000)
.then((res) => {
console.log('奶茶製作完成!!!')
return buyThing(20, 2000)
})
.then((res) => {
console.log('冰淇淋製作完成!!!')
})
在程式碼層面,使用 Promise 之後,解決了多層回撥函式呼叫導致的“金字塔”現象。讓我們看看實現效果:
請轉至 MDN 關於 Promise 的解釋:Promise - JavaScript | MDN