专栏名称: __ihhu
前段
目录
相关文章推荐
前端早读课  ·  【第3455期】快手主站前端工程化探索:Gu ... ·  17 小时前  
歸藏的AI工具箱  ·  终于有给设计师用的 Cursor 了 ·  昨天  
歸藏的AI工具箱  ·  终于有给设计师用的 Cursor 了 ·  昨天  
前端早读课  ·  【第3454期】如何用语音学习编程的 ·  昨天  
前端大全  ·  前端行情变了,差别真的挺大。。。 ·  2 天前  
前端早读课  ·  【开源】TinyEngine开启新篇章,服务 ... ·  2 天前  
51好读  ›  专栏  ›  __ihhu

Generator的正确打开方式

__ihhu  · 掘金  · 前端  · 2018-05-16 06:45

正文

前两年大量的在写 Generator + co ,用它来写一些类似同步的代码
但实际上, Generator 并不是被造出来干这个使的,不然也就不会有后来的 async await
Generator 是一个可以被暂停的函数,并且何时恢复,由调用方决定
希望本文可以帮助你理解 Generator 究竟是什么,以及怎么用

放一张图来表示我对 Generator 的理解:

一个咖啡机,虽说我并不喝咖啡,可惜找不到造王老吉的机器-.-

我所理解的 Generator 咖啡机大概就是这么的一个样子的:

  1. 首先,我们往机器里边放一些咖啡豆
  2. 等我们想喝咖啡的时候,就可以按开关( gen.next() ),机器开始磨咖啡豆、煮咖啡、接下来就得到咖啡了
  3. 等接满了一杯咖啡后,阀门就会自动关闭( yield )
  4. 如果你一开始往机器里边放的咖啡豆很多的话,此时,机器里边还是会有一些剩余的,下次再想喝还可以继续按开关,执行(磨豆、煮咖啡、接咖啡)这一套操作

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 text
gen.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) // 11
method(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 // 11
gen.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 // 1
gen.next().value // 2
gen.next().value // 3
gen.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 // 返回一个随机数

代替一些递归的操作

那个最著名的 斐波那契数 ,基本上都会选择使用递归来实现







请到「今天看啥」查看全文