[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