[JS]回撥函式和回撥地獄

shiramashiro發表於2021-08-06

回撥函式

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

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

相關文章