ES6 - Generator

900 阅读1分钟

定义

Generator 本意是生成器,普通函数一旦被执行,那么在结束之前是不再执行其他的JS代码。但是Generator函数可以在函数执行的过程中暂停,又可以在暂停的地方继续执行

Generator函数的特点是

  • 声明函数的时候在函数名前有星号
  • 里面有yield关键字,本意是生产、产出的意思
  • Generator需要我们手动的去执行,使用f.next()
  • Generator函数并不会立即执行,而是返回一个生成器的迭代器对象
  • 当迭代器函数调用next方法的时候,内部就会执行到yield后面的语句为止
  • Generator函数不能当做构造函数去使用,它只能返回生成器的对象
  • yield关键字只能在Generator函数里面去使用,不能在其他地方使用
function* foo(args) {
    args.forEach(item => {
        yield item + 1
    })
}

image.png

基础例子

例1:普通函数调用同Generator函数调用的区别

普通的函数

function foo() {
    for(let i=0; i< 3; i++) {
        console.log(i);                     // 0 1 2
    }
}
foo()

Generator函数 / 生成器函数

function* foo() {
    for(let i = 0; i < 3; i++){
        yield i
    }
}
let f = foo()
console.log(f.next())
console.log(f.next())
console.log(f.next())
console.log(f.next())

image.png

让函数有点像在调试的时候使用的单步执行一样,一步一步的手动的控制进行到哪一步

备注:

  • next()  方法返回一个包含属性 done 和 value 的对象,value表示当前这一次yield的返回值
  • done表示当前的Generator是否有执行完成或者说后续是否还有yield语句,当函数执行完成则done的值就变成true

例 2 : next()中没有传参

function* gen(x) {
    let y = 2 * (yield(x + 1))
    let z = yield(y / 3)
    return x + y + z
}

let g = gen(5)
console.log(g.next())       // 6
console.log(g.next())       // NaN
console.log(g.next())       // NaN

image.png

分析下结果为什么是这样?

  • next()中可以传参数,它表示关于generator内部的上一条yield语句的返回值
  • 第一次调用next()是执行到yield后面的x + 1,x=5,结果是6
  • next()中的参数表示上一条yield的返回值,当第二次调用next()的时候没有传参,因此(yield(x + 1))返回的结果是undefined,因此y是NaN.第二次next()执行到yield后面的代码(y / 3)也是NaN,因此第二次的next的执行结果是NaN
  • 最后一次的next中也没有传参,因此上一次yield的返回值 y / 3 也是 undefined,z是undefined,因此结果是NaN

例 3 : next()中传参

function* gen(x) {
    let y = 2 * (yield(x + 1))
    let z = yield(y / 3)
    return x + y + z
}

let g = gen(5)
console.log(g.next())               // 6
console.log(g.next(12))             // 8
console.log(g.next(13))             // 42

image.png

备注:

  • next()执行的结果是yield后面的表达式
  • 第一次next执行的值是x+1,等于6
  • next传递参数表示上一次yield表达式返回的值
  • next中的参数12的意思就是(yield(x + 1)) = 12,y=24,但是next表示下一次yield结束的位置才停止也就是y/3,因此得到的结果是8
  • 当第三次调用next,13表示的上一次yield表达式的返回值,yield(y / 3) = 13, z = 13
  • x = 5,y = 24,z = 13,x+y+z = 42
  • 注意:要把计算xyz的值同g.next()的返回值分开分析

通过上面的例子我们知道:next()是如何执行的、next()的参数是如何使用的、yield的作用

例4 :敲7游戏,逢7的倍数跳过

使用generator函数求出7的倍数

function* count(x = 1){
    while (true) {
        if(x % 7 === 0){
            yield x
        }
        x++
    }
}

let n = count()
console.log(n.next().value);
console.log(n.next().value);
console.log(n.next().value);
console.log(n.next().value);
console.log(n.next().value);

image.png

如果没有Generator那么我们写的函数就陷入死循环,正因为有Generator可以让我们在中途停止,当前才不会是死循环

Generator如何对异步的状态进行管理呢?

还是用之前发送ajax请求来获取static/a.json、static/b.json 、static/c.json、,它们之间存在依赖关系,先请求a再请求b最后请求c

前2次分别使用了回调深渊和Promise实现,这里将使用Generator的方式对异步操作进行管理


function ajax(url,callback) {
    // 1. 创建XMLHttpRequest对象
    var xmlhttp
    if(window.XMLHttpRequest) {
        xmlhttp = new XMLHttpRequest()
    // 兼容早期浏览器
    } else {
        xmlhttp = new ActiveXObject('Microsoft.XMLHTTP')
    }
    // 2. 发送请求
    xmlhttp.open('GET',url,true)
    xmlhttp.send()
    // 3. 接收服务端相应
    xmlhttp.onreadystatechange = function() {
        if(xmlhttp.readyState === 4 && xmlhttp.status === 200){
            var obj = JSON.parse(xmlhttp.responseText)
            callback(obj);
        }
    }
    
}

function request(url) {
    ajax(url,(res) => {
        // 下面这句很难理解
        getData.next(res) // next参数代表上一次yield的返回值,这样就把请求的结果返回到了generator函数中yield的request的请求结果
    })
}

function* gen(){
    let res1 = yield request('static/a.json')
    console.log(res1);
    let res2 = yield request('static/b.json')
    console.log(res2);
    let res3 = yield request('static/c.json')
    console.log(res3);
}
let getData = gen()
getData.next()

image.png

备注:

  • 第一次调用next的时候会执行到yield的位置停止,request发送请求a.json,进入request,当进入getData.next(res) 执行next的时候,参数res,就是上一次yield的返回值,也就是yield request('static/a.json')它的返回值,就得到了res1的结果
  • 同理可以得到res2、res3的结果