一篇 Generator 详解

avatar
Web前端 @CVTE_希沃

希沃ENOW大前端

公司官网:CVTE(广州视源股份)

团队:CVTE旗下未来教育希沃软件平台中心enow团队

本文作者:

大大大正名片.jpg

前言

遥想当初面试的时候,被问到经典问题ES6都有啥,年纪轻轻的我搜刮了满脑子,除了水就是水。

image.png

那么ES6都有啥呢,很快脑子里就有let和const、模板字符串、解构赋值、展开运算符、箭头函数等等。说了这些也许足够让面试官展开发挥了,但是优秀的掘金er怎么可能止步于此?

image.png

为了让面试官眼前一亮,es6还有这些新特性:模块化、Symbol、Set和Map数据结构、Proxy代理和Reflect反射、Generator等等,说完面试官估计眼睛开始有光了,那么说了就要懂,懂了才不怕被面试官问,虽然有点冷门,俗话说技多不压身,今天就一起来看看Generator吧!

image.png

generator是什么

generator即生成器,是ES6规范带来的新内容,在generator能够让我们在函数执行时任意地方暂停,在后续遇到合适的时机需要使用这个函数时继续执行。以往我们遇到的函数都是一口气执行到底,而generator的特点就是让函数执行到中间“刹车”,在需要它的时候接着执行。下面从一个案例来看看generator的基本用法吧!

image.png

常见的javascript函数:

function fun() {
  console.log(1)
  console.log(2)
  console.log(3)
}

function run() {
  console.log(4)
}

fun()
run()

// 结果:
1
2
3
4

使用generator函数:

function* funG() {
  yield console.log(1)
  yield console.log(2)
  yield console.log(3)
}

function run() {
  console.log(4)
}

const iter = funG()
iter.next()
run()
iter.next()
iter.next()
iter.next()

// 结果:
1
4
2
3
{value: undefined, done:true}

写法上:

  1. generator相对于普通函数在function后面多加了*号。
  2. 在每个我们需要中断执行的语句前加了yield,通过yield来控制函数执行。

从打印结果上来看:

  1. 普通函数一口气打印了1,2,3,4。
  2. generator打印结果明显不同,当调用generator函数的时候并不是立即执行,返回的是一个生成器内部指针对象iter,通过调用.next()方法移动指针对象到下一个yield,执行表达式,返回表达式结果并暂停自身。每执行一次,都会返回一个包含value和done属性的对象,value为当前表达式的值,done是boolean值,当done的值为true的时候,表示生成器执行完成。

知道generator的基本用法了,但还不够,为了加深面试官的印象,我们可以从generator设计出发点聊聊协程!

image.png

generator与协程


既然要聊协程,首先得知道协程是什么吧!

简单来说协程就像单身程序员小王敲代码,老大给了他一个项目A,小王收到立马开码;

小王项目A做到一半,老大说有个项目B时间赶,赶紧来干项目B;

于是小王停止开发项目A,着手开干项目B;

项目B开发一段时间后,小王回来接着干项目A。

这就是协程,那么项目B做完了?也许没有。

image.png

看完了协程的案例,聪明的你应该想到了协程跟generator之间的关系!没错,generator就是协程在js上的实现。通过generator,我们可以在单线程的JavaScript里使用协程!

generator的特性用法

generator本身作为异步编程的解决方案,可以用来解决异步任务。除此之外还可以有更灵活的用法!那便是在函数执行过程中传入参数,以及获取该段表达式输出结果!

上文提到generator每次执行next()方法都会返回一个对象:{ value, done },通过value,我们可以获取该段表达式的返回结果,另外,next还可以接受参数,利用这一特性,我们可以随时传入参数!

function* run(name) {
    let who = yield name + ' Allen';
    return who
}

let flashMan = run('Barry')
flashMan.next() // { value: 'Barry Allen', done: false }
// 传入参数
flashMan.next('Arrow') // { value: 'Allow', done: true }


在第一个next()调用时,不传入参数,默认将name的参数值'Barry'与' Allen'组合字符串,这个值被yield返回。

在第二个next()调用时,传入参数'Arrow',这个值被变量who接收,因此返回value属性的值为'Arrow',也就是who的值。

不仅如此,generator还可以在函数运行时,捕获函数体外抛出的错误。

function* gen(x){
  try {
    var y = yield x + 2;
  } catch (e){ 
    console.log(e);
  }
  return y;
}

var g = gen(1);
g.next();
g.throw'error');
// error

generator的简单实现


generator的原理是转化为switch-case来实现的,先从一个简单的案例入手。

function* funG() {
  yield 1
  yield 2
  yield 3
}

const iter = funG()
iter.next() // {value: 1, done: false}
iter.next() // {value: 2, done: false}
iter.next() // {value: 3, done: false}
iter.next() // {value: undefined, done: true}

generator的实现需要一个函数,这个函数可以多次调用,每次返回一个结果,还可以传入参数,那么switch-case就是很好的选择。

function funGen(count) {
  switch(count) {
    case 1:
      return {value: 1, done: false};
    case 2: 
      return {value: 2, done: false};
    case 3:
      return {value: 3, done: false};
    case 'done':
      return {value: undefined, done: true}
  }
}

funGen(1) // {value: 1, done: false};
funGen(2) // {value: 2, done: false};
funGen(3) // {value: 3, done: false};
funGen('done') // {value: undefined, done: true}

从结果上来看是我们想要的结果,但是距离generator还有很大的差距,接下来创建一个函数,这个函数返回一个对象,通过调用这个对象来帮我们执行14~17行的语句。

function funGen(count) {
  switch(count) {
    case 1:
      return {value: 1, done: false};
    case 2: 
      return {value: 2, done: false};
    case 3:
      return {value: 3, done: false};
    case 'done':
      return {value: undefined, done: true}
  }
}

const gen = function() {
  let count = 0
  return {
    next: function() {
      ++count
      count = count > 3 ? 'done' : count
      return funGen(count)
    }
  }
}

const test = gen()
test.next() // {value: 1, done: false}
test.next() // {value: 2, done: false}
test.next() // {value: 3, done: false}
test.next() // {value: undefined, done: true}

到目前为止,都需要我们手动处理函数上下文里的count的值的变化来决定返回结果,所以我们需要一个对象来保存函数上下文也就是count的值。

function example(context) {
  while(1) {
    context.pre = context.next
    switch(context.pre) {
      case 1:
        context.next = 2
        return 1;
      case 2: 
        context.next = 3
        return 2;
      case 3:
        context.next = 'done'
        return 3;
      case 'done':
        return context.end()
    }
  }
}

const gen = function() {
  return {
    next: function() {
      value = context.done ? undefined : funGen(context)
      done = context.done
      return {
        value,
        done
      }
    }
  }
}

const context = {
  pre: 1,
  next: 1,
  done: false,
  end: function end() {
    this.done = true
  }
}

const test = gen()
test.next() // {value: 1, done: false}
test.next() // {value: 2, done: false}
test.next() // {value: 3, done: false}
test.next() // {value: undefined, done: false}


通过一个新对象context来记录函数上下文的初始状态pre,以及下一个状态next,done记录是否到最终点。而end便是修改done为结束状态true。

funGen每次运行next()结束后都会将运行状态保存到context中,方便下次运行时获取。

总结

gernerator作为es6的新特性,在后来被更方便好用的async await代替,但是generator独特的特性可以让我们在函数执行的过程中传递参数获取结果,使得函数调用变得更加灵活。作为一个开发者,我们有必要了解一下gernerator的基本使用以及简单的实现的原理,方便在特殊的场景中解决问题。