JS 如何用 thunk 函数解决回调地狱?
这是我参与2022首次更文挑战的第19天,活动详情查看:2022首次更文挑战」。
导语
在多个异步嵌套调用的时候就往往会产生很多个回调,导致很多个回调函数的嵌套,这就产生了回调地狱。回调地狱在开发中是非常难读的,为了保证代码的可读性和优雅,我们常常会想方设法去避免出现回调地狱,那么使用 thunk 函数就是解决方法之一。thunk函数在日常的开发中可能不会被经常用到,因为他是JS函数的高阶用法,但是在 一些大厂的面试中,或者一些优秀的源码库中你可能会经常遇到,如果不理解什么是thunk函数的话,将会很难读懂源码了。所以我们将会讨论什么是回调地狱,什么是 thunk 函数,如何利用 thunk 函数去解决回调地狱。
回调地狱
先看代码:
const fn = function (str, callback) {
setTimeout(() => {
console.log(str)
if (callback) {
callback()
}
}, 100);
}
fn('1', () => {
fn('2', () => {
fn('3', () => {
console.log('任务执行完毕')
})
})
})
打印结果:
1
2
3
任务执行完毕
有函数 fn
,接收入参 str
和 回调函数 callback
。在 fn
延时打印了 str
,然后调用回调函数。
一个非常简单的函数,有这样的一个场景,你需要调用3次,并且每次都是在结果返回之后进行调用下一次。那么就会如上述代码所见到的,fn
被多层嵌套,导致代码可读性非常差,并且看起来非常臃肿,虽然能够实现功能,但不足够优雅。这就是我们常说的回调地狱。
同样的情况,我们在实际开发中也非常可能遇到:
假设有3个接口,接口必须顺序调用,那么就必须等待第一个接口返回结果之后再次调用第二个接口,等待第二个接口返回结果之后再去调用第三个接口,就很可能出现以上回调地狱的情况了。
当然我们可以使用 async/await 等等 的方式去解决,但本次讨论的重点是使用 thunk函数去解决。
thunk 函数
什么是 thunk 函数? 编译器的"传名调用"实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。
thunk作为临时函数,是有它一定目的的临时函数。 thunk 函数的目的就是将参数和回调分离。
1. 写一个最简单的 thunk 函数
看以下代码:
const fn = function (str, callback) {
setTimeout(() => {
console.log(str)
if (callback) {
callback()
}
}, 100);
}
const thunk = (str) => {
return (callback) => {
return fn(str, callback)
}
}
这里有一个 函数名为 thunk, 他接收的入参为字符串, 内容是返回一个带有 callback
入参的函数b, b函数去执行 fn
并且返回结果。
这样以来我们可以分析一下:
-
thunk函数的入参变成了字符串,将不再有回调。
-
执行 thunk函数的时候会返回一个带有回调作为入参的函数。
-
返回的函数会去真正执行
fn
尝试调用一下:
const thunk1 = thunk('1')
const thunk2 = thunk('2')
const thunk3 = thunk('3')
cconsole.log(thunk1) // [Function (anonymous)]
这里的thunk1,thunk2,thunk3 仍然是函数。
调用方式:
const thunk1 = thunk('1')
const thunk2 = thunk('2')
const thunk3 = thunk('3')
thunk1(() => { thunk2(() => { thunk3() }) }) // 1 2 3
虽然和开始的结果一致,但是这样似乎和开始得调地狱没有区别,体现不出 thunk 函数的优势。
继续优化:
2. 科里化函数
首先 thunk 函数可以发现是一个函数的科里化,那么可以这么写:(个人感觉叫做偏应用函数更加合适,str参数被固定,返回一个用来接收剩余参数callback的函数)
// const thunk = (str) => {
// return (callback) => {
// return fn(str, callback)
// }
// }
const thunk = str => callback => fn(str, callback) // thunk 科里化简写
其实本质是一样的。
3. 将 thunk 结果封装在数组
const thunk = str => callback => fn(str, callback) // thunk 科里化简写
const thunk1 = thunk('1')
const thunk2 = thunk('2')
const thunk3 = thunk('3')
// thunk1(() => { thunk2(() => { thunk3() }) })
const arr = [thunk1, thunk2, thunk3]
const generate = (arr) => {
arr[0](() => arr[1](() => arr[2]())) // 1 2 3
}
generate(arr)
我们将 thunk 结果函数们封装在了一个数组里,然后依然嵌套调用,本质是和之前的代码一样的,只不过是从数组里面取直而已。为什么要放在数组里面呢?原因是方便创建,创建一个 thunk放入数组就不用管了,我们只需要关注如何实现generate
函数就行了。
到目前为止,和之前的还是没有本质的区别,依然是嵌套调用。但我们已经为优化做好了铺垫。
4. reduceRight 叠加执行
在 generate
函数中,我们可以让 arr 内的元素叠加执行。
const generate = (arr) => {
arr.reduceRight((a, b) => () => {
return b(() => { a() })
})()
}
generate(arr) // 1 2 3
5. 完整代码
const fn = function (str, callback) {
setTimeout(() => {
console.log(str)
if (callback) {
callback()
}
}, 100);
}
const thunk = str => callback => fn(str, callback) // thunk 科里化简写
const thunk1 = thunk('1') // 创建 thunk
const thunk2 = thunk('2')
const thunk3 = thunk('3')
const arr = [thunk1, thunk2, thunk3] // 将thunk函数放入数组
const generate = (arr) => {
arr.reduceRight((a, b) => () => {
return b(() => { a() })
})()
}
generate(arr) // 1 2 3
这样就完成了 通过 thunk 解决回调地狱了!
在实际开发中,我们可以将 generate
方法,以及写一个 createTrunk
方法 作为工具类中的方法,在遇到回调地狱的时候直接开箱使用就可以了。
这样,你的私有工具包中是不是又多了一个 util 方法了?!
6.多参数 + 回调
案例中只有一个参数,这里适不适用多个参数呢?
const fn = function (geeting, yourName, callback) {
setTimeout(() => {
console.log(geeting + ',' + yourName)
if (callback) {
callback()
}
}, 100);
}
const thunk = (greeting, yourName) => callback => fn(greeting, yourName, callback) // thunk 科里化简写
const thunk1 = thunk('hello', '1') // 创建 thunk
const thunk2 = thunk('hi', '2')
const thunk3 = thunk('ok', '3')
const arr = [thunk1, thunk2, thunk3] // 将thunk函数放入数组
const generate = (arr) => {
arr.reduceRight((a, b) => () => {
return b(() => { a() })
})()
}
generate(arr)
// hello,1
// hi,2
// ok,3
答案是肯定的。