前两年大量的在写
但实际上,Generator
+co
,用它来写一些类似同步的代码Generator
并不是被造出来干这个使的,不然也就不会有后来的async
、await
了Generator
是一个可以被暂停的函数,并且何时恢复,由调用方决定 希望本文可以帮助你理解Generator
究竟是什么,以及怎么用
放一张图来表示我对Generator
的理解:
我所理解的Generator咖啡机大概就是这么的一个样子的:
- 首先,我们往机器里边放一些咖啡豆
- 等我们想喝咖啡的时候,就可以按开关(
gen.next()
),机器开始磨咖啡豆、煮咖啡、接下来就得到咖啡了 - 等接满了一杯咖啡后,阀门就会自动关闭(
yield
) - 如果你一开始往机器里边放的咖啡豆很多的话,此时,机器里边还是会有一些剩余的,下次再想喝还可以继续按开关,执行(磨豆、煮咖啡、接咖啡)这一套操作
拿Generator
将上述咖啡机实现一下:
function * coffeeMachineGenerator (beans) { do { yield cookCoffee() } while (--beans) // 煮咖啡 function cookCoffee () { console.log('cooking') return 'Here you are' }}// 往咖啡机放咖啡豆let coffeeMachine = coffeeMachineGenerator(10)// 我想喝咖啡了coffeeMachine.next()// 我在3秒后还会喝咖啡setTimeout(() => { coffeeMachine.next()}, 3 * 1e3)复制代码
代码运行后,我们首先会得到一条cooking
的log
,
3s
后会再次得到一条log
。 这就解释了Generator
是什么:
next
来获取数据(我们自己来决定是否何时煮咖啡) 在遇到yield
以后函数的执行就会停止(接满了一杯,阀门关闭) 我们来决定何时运行剩余的代码next
(什么时候想喝了再去煮) 这是Generator
中最重要的特性,我们只有在真正需要的时候才获取下一个值,而不是一次性获取所有的值
Generator的语法
声明Generator
函数有很多种途径,最重要的一点就是,在function
关键字后添加一个*
function * generator () {}function* generator () {}function *generator () {}let generator = function * () {}let generator = function* () {}let generator = function *() {}// 错误的示例let generator = *() => {}let generator = ()* => {}let generator = (*) => {}复制代码
或者,因为是一个函数,也可以作为一个对象的属性来存在:
class MyClass { * generator() {} *generator2() {}}const obj = { *generator() {} * generator() {}}复制代码
generator的初始化与复用
一个Generator
函数通过调用两次方法,将会生成两个完全独立的状态机
Generator
对象很重要: function * generator (name = 'unknown') { yield `Your name: ${name}`}const gen1 = generator()const gen2 = generator('Niko Bellic')gen1.next() // { value: Your name: unknown , done: false}gen2.next() // { value: Your name: Niko Bellic, done: false}复制代码
Method: next()
最常用的next()
方法,无论何时调用它,都会得到下一次输出的返回对象(在代码执行完后的调用将会始终返回{value: undefined, done: true}
)。
next
总会返回一个对象,包含两个属性值:
value
:yield
关键字后边表达式的值done
:如果已经没有yield
关键字了,则会返回true
. function * generator () { yield 5 return 6}const gen = generator()console.log(gen.next()) // {value: 5, done: false}console.log(gen.next()) // {value: 6, done: true}console.log(gen.next()) // {value: undefined, done: true}console.log(gen.next()) // {value: undefined, done: true} -- 后续再调用也都会是这个结果复制代码
作为迭代器使用
Generator
函数是一个可迭代的,所以,我们可以直接通过for of
来使用它。
function * generator () { yield 1 yield 2 return 3}for (let item of generator()) { item}// 1// 2复制代码
return
不参与迭代
yield
,也就是说,在迭代后的Generator
对象将不会再返回任何有效的值 Method: return()
我们可以在迭代器对象上直接调用return()
,来终止后续的代码执行。
return
后的所有next()
调用都将返回{value: undefined, done: true}
function * generator () { yield 1 yield 2 yield 3}const gen = generator()gen.return() // {value: undefined, done: true}gen.return('hi') // {value: "hi", done: true}gen.next() // {value: undefined, done: true}复制代码
Method: throw()
在调用throw()
后同样会终止所有的yield
执行,同时会抛出一个异常,需要通过try-catch
来接收:
function * generator () { yield 1 yield 2 yield 3}const gen = generator()gen.throw('error text') // Error: error textgen.next() // {value: undefined, done: true}复制代码
Yield的语法
yield
的语法有点像return
,但是,return
是在函数调用结束后返回结果的
return
之后不会执行其他任何的操作 function method (a) { let b = 5 return a + b // 下边的两句代码永远不会执行 b = 6 return a * b}method(6) // 11method(6) // 11复制代码
而yield的表现则不一样
function * yieldMethod(a) { let b = 5 yield a + b // 在执行第二次`next`时,下边两行则会执行 b = 6 return a * b}const gen = yieldMethod(6)gen.next().value // 11gen.next().value // 36复制代码
yield*
yield*
用来将一个Generator
放到另一个Generator
函数中执行。
[...]
的功能: function * gen1 () { yield 2 yield 3}function * gen2 () { yield 1 yield * gen1() yield 4}let gen = gen2()gen.next().value // 1gen.next().value // 2gen.next().value // 3gen.next().value // 4复制代码
yield的返回值
yield
是可以接收返回值的,返回值可以在后续的代码被使用
function * generator (num) { return yield yield num}let gen = generator(1)console.log(gen.next()) // {value: 1, done: false}console.log(gen.next(2)) // {value: 2, done: false}console.log(gen.next(3)) // {value: 3, done: true }复制代码
我们在调用第一次next
时候,代码执行到了yield num
,此时返回num
next(2)
,代码执行的是yield (yield num)
,而其中返回的值就是我们在next
中传入的参数了,作为yield num
的返回值存在。 以及最后的next(3)
,执行的是这部分代码return (yield (yield num))
,第二次yield
表达式的返回值。 一些实际的使用场景
上边的所有示例都是建立在已知次数的Generator
函数上的,但如果你需要一个未知次数的Generator
,仅需要创建一个无限循环就够了。
一个简单的随机数生成
比如我们将实现一个随机数的获取:
function * randomGenerator (...randoms) { let len = randoms.length while (true) { yield randoms[Math.floor(Math.random() * len)] }}const randomeGen = randomGenerator(1, 2, 3, 4)randomeGen.next().value // 返回一个随机数复制代码
代替一些递归的操作
那个最著名的斐波那契数,基本上都会选择使用递归来实现
但是再结合着Generator
以后,就可以使用一个无限循环来实现了: function * fibonacci(seed1, seed2) { while (true) { yield (() => { seed2 = seed2 + seed1; seed1 = seed2 - seed1; return seed2; })(); }}const fib = fibonacci(0, 1);fib.next(); // {value: 1, done: false}fib.next(); // {value: 2, done: false}fib.next(); // {value: 3, done: false}fib.next(); // {value: 5, done: false}fib.next(); // {value: 8, done: false}复制代码
与async/await的结合
再次重申,我个人不认为async/await是Generator的语法糖。。
如果是写前端的童鞋,基本上都会遇到处理分页加载数据的时候
如果结合着Generator
+async
、await
,我们可以这样实现: async function * loadDataGenerator (url) { let page = 1 while (true) { page = (yield await ajax(url, { data: page })) || ++page }}// 使用setTimeout模拟异步请求function ajax (url, { data: page }) { return new Promise((resolve) => { setTimeout(_ => { console.log(`get page: ${page}`); resolve() }, 1000) })}let loadData = loadDataGenerator('get-data-url')await loadData.next()await loadData.next()// force load page 1await loadData.next(1)await loadData.next()// get page: 1// get page: 2// get page: 1// get page: 2复制代码
这样我们可以在简单的几行代码中实现一个分页控制函数了。
如果想要从加载特定的页码,直接将page
传入next
即可。 小记
Generator
还有更多的使用方式,(实现异步流程控制、按需进行数据读取) 个人认为,Generator
的优势在于代码的惰性执行,Generator
所实现的事情,我们不使用它也可以做到,只是使用Generator
后,能够让代码的可读性变得更好、流程变得更清晰、更专注于逻辑的实现。
如果有什么不懂的地方 or 文章中一些的错误,欢迎指出