带你了解JS Generator函数以及用它来解决回调地狱
这是我参与2022首次更文挑战的第25天,活动详情查看:2022首次更文挑战」。
导语
在JS中有一种函数的写法和普通函数有所不同,也是ES6中提供的一套异步处理的解决方案。本文将讨论Generator函数相关的一些知识点以及应用。
什么是 Generator?
Generator 函数和普通的函数写法有些许不同,它需要在 function后面加上*,来表示当前函数是 Generator函数。
// 普通函数
function fun () {
}
// generator 函数
function* generator () {
}
Generator 函数可以用来控制函数执行的暂停和启动,在 Generator函数中,yield 会起到标识暂停的标志。
Generator 中的 yield 和 next()
yield: 在概念中提到Generator函数可以控制函数执行的暂停,那么函数在哪里会暂停就是由关键字 yield来决定的。
next(): next 方法是用来控制Generator函数继续执行的方法,也是generator函数的启动方法。当Generator函数遇到 yield 时会暂停执行,通过调用 next 方法是函数继续执行。
举个例子:
function* generator() {
console.log('a')
yield
console.log('b')
}
const gen = generator()
gen.next() // 启动 generator
执行结果 a
可以看到,generator函数中有 a , b两次打印,但是只打印了a,原因是generator函数执行到 yield 关键字的时候就暂停了。
如果再次执行 gen.next(),则函数将会继续执行,打印 b。
yield 和 next() 的传值
直接举个例子:
function* generator() {
yield '1'
yield '2'
}
const gen = generator()
console.log(gen.next())
console.log(gen.next())
执行结果:
{ value: '1', done: false }
{ value: '2', done: false }
通过 yield 关键字可以将 generator 函数暂停时的结果传递给 next 函数返回。返回对象为 {value: xxx, done: true/false}
yield 关键字可以将 generator 函数暂停时的结果传递给 next 函数返回这句话就可以直接理解为: yield 等于 return, 但函数的生命周期并没有执行完,只是暂停。yield 只是将数据返回给 next 函数。
done 这个字段用来表示函数是否已经执行完毕,这里可能会有疑问,明明第二个 yield 就已经是函数的结尾了,为什么还是返回 false?那是因为遇到了 yield 函数的执行就被暂停住了,所以函数并不会知道后面是是否还有代码需要执行,不能从直观地去看函数已经到结尾了,这里如果再执行第三个 gen.next(),则会有如下结果:
{ value: undefined, done: true }
当函数执行完毕时,也会通过 next 函数回调,这时 value 是 undefined, done 即为 true 了。
接下来,再聊聊如何向 generator 函数中传值:
function* generator() {
const a = yield 1
console.log(`我是a: ${a}`)
}
const gen = generator()
console.log(gen.next())
console.log(gen.next())
打印结果:
{ value: 1, done: false }
{ value: undefined, done: false }
这里的 a 在第二次 的 next 会被返回并且打印,但是 a 这个值不是我们可能想到的是 1,说明 yield 并不会在函数里面返回值。那么这个 a 是如何被赋值的?
function* generator() {
const a = yield 1
console.log(`我是a: ${a}`)
}
const gen = generator()
console.log(gen.next())
console.log(gen.next(2))
打印结果:
{ value: 1, done: false }
我是a: 2
{ value: undefined, done: true }
通过 next() 传递参数,就可以理解为给 yield 关键字返回数据。
为了更好帮助大家理解,做了下面这个流程图:
generator 函数容易犯错的地方
function* generator() {
const a = yield 1
console.log(`我是a: ${a}`)
}
const gen = generator()
console.log(gen.next(2))
console.log(gen.next())
结果:
{ value: 1, done: false }
我是a: undefined
{ value: undefined, done: true }
函数传递写在了第一个 next 里面,认为第一个 next 就是第一个 yield 的返回值,实际上不是的,传值是在启动第一个 yield 的时候进行的,那么第一个 next 我们可以认为是对 generator 函数的启动,真正在 yield 暂停的地方启动的实际上是第二个 next,所以传值应该写在第二个 next 里面。
generator 的遍历
如果我们不使用 next 方法,能否一次性获取 yield 的结果呢?
function* generator() {
yield 1
yield 2
yield 3
yield 4
}
const gen = generator()
for (let value of gen) {
console.log(value)
}
打印结果:
1
2
3
4
使用 for ... of 方法,即可直接遍历出所有 yield 的返回值,不需要判断 done 是否为 true 以及不需要从对象中获取 value 字段对应的值。
中止 generator
function* generator() {
yield 1
yield 2
yield 3
}
const gen = generator()
console.log(gen.next())
console.log(gen.next())
console.log(gen.return())
console.log(gen.next())
打印结果:
{ value: 1, done: false }
{ value: 2, done: false }
{ value: undefined, done: true }
{ value: undefined, done: true }
这里的 generator 函数在执行到第三个 yield 就已经结束了,dong = true,可以通过 gen.return()代替 gen.next(),来强行终止 generator 函数的执行。
Generator 函数的应用: 利用 thunk 函数 + Generator 函数 解决回调地狱
说了这么多理论和栗子,终于来到讨论的重点:应用场景!一起来实现一下 thunk 函数 + Generator 函数解决回调地狱。
先看代码:
function delayLog(str, callback) {
setTimeout(() => {
console.log(str, new Date().getTime())
callback()
}, 1000);
}
delayLog(1, () => {
delayLog(2, () => {
delayLog(3, () => {
console.log('done')
})
})
})
结果:
结果:
1 1645453408723
2 1645453409731
3 1645453410733
done
这是一个典型的回调嵌套,即我们常说的回调地狱。看上去很不优雅。下面我们一步一步实现调用delayLog的化繁为简。
1. 实现 thunk 工厂函数
const thunkFactory = (str) => {
return function (callback) {
delayLog(str, callback)
}
}
利用偏应用函数,将 str 和 callback 分离。
2. 为每一次调用创建一个 thunk 函数,并储存在数组中
const thunkList = [
thunkFactory('1'),
thunkFactory('2'),
thunkFactory('3'),
]
3. 创建 generator 函数
const generator = function* (list) {
for (let index = 0; index < list.length; index++) {
yield list[index]()
}
console.log('done')
}
const gen = generator(thunkList)
gen.next()
创建一个 generator 函数,内容是遍历执行所有的 thunk 函数,执行一次便暂停。
4. 修改 delayLog
function delayLog(str, callback) {
setTimeout(() => {
console.log(str, new Date().getTime())
if (callback) {
callback()
}
if (gen) {
gen.next()
}
}, 1000);
}
在 delayLog 中增加了 gen.next(),表示当函数执行完后,会从 yield 处继续执行。
5.执行结果
1 1645454318590
2 1645454319594
3 1645454320595
done
至此完成了一个 thunk 函数 + generator 函数解决回调地狱的例子。调用起来非常简单:
const thunkList = [ thunkFactory('1'), thunkFactory('2'), thunkFactory('3'), ]
只要在 thunkList 中创建添加 thunk 对象即可。
6. 完整代码
function delayLog(str, callback) {
setTimeout(() => {
console.log(str, new Date().getTime())
if (callback) {
callback()
}
gen.next()
}, 1000);
}
const thunkFactory = (str) => {
return function (callback) {
delayLog(str, callback)
}
}
const thunkList = [ thunkFactory('1'), thunkFactory('2'), thunkFactory('3'),]
const generator = function* (list) {
for (let index = 0; index < list.length; index++) {
yield list[index]()
}
console.log('done')
}
const gen = generator(thunkList)
gen.next()