JS 如何用 thunk 函数解决回调地狱?

257 阅读5分钟

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并且返回结果。

这样以来我们可以分析一下:

  1. thunk函数的入参变成了字符串,将不再有回调。

  2. 执行 thunk函数的时候会返回一个带有回调作为入参的函数。

  3. 返回的函数会去真正执行 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

答案是肯定的。