[JS]回調函數和回調地獄

回調函數

小明在奶茶店點了奶茶,店員開始製作奶茶,此時「製作奶茶」與「小明等待奶茶」是一個同時進行的不同的兩個事件(任務),那麼,小明獲取店員製作成功的奶茶是從「製作奶茶」這一事件獲取的結果,所以小明才能夠完成「購買奶茶」這一事件。如果,小明在「購買奶茶」這一事件中,不想一直等待而是想去做一些其他的事情,比如購買雪糕。

現在,我們將這一案例抽取為一個個事件,用 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