likes
comments
collection
share

详聊es6的Generator

作者站长头像
站长
· 阅读数 4

generator也是解决异步的一种方式,这对我们后续学习async源码会很有帮助,尽管generator在异步发展史中仅仅是昙花一现,但是既然要对线面试官,我们还是得把它给搞明白

文章参考:Generator 函数的语法 - ECMAScript 6入门 (ruanyifeng.com)

语法

只要给一个函数关键字后面添加一个星号*,那么这个函数就被称之为生成器generator函数,此时并无特殊之处,我们现在可以去浏览器打印看看这个函数的执行结果长什么样

详聊es6的Generator

里面有个constructor,说明这样写就相当于new了一个生成器函数,并且,里面有next关键字,这个关键字是放在原型身上的,说名我们的实例对象是可以去写.next

既然如此,那我把它的执行结果赋值给另一个参数gen,然后让gen.next,我们试试看。当然我们需要在generator函数里面写上yield关键字,yield是产出的意思

function* foo () {
    yield 'a'
    yield 'b'
    yield 'c'
    return 'ending'
}
let gen = foo() // 非执行,相当于new了一个实例对象,这就是*作用
console.log(gen.next());  // { value: 'a', done: false }
console.log(gen.next());  // { value: 'b', done: false }
console.log(gen.next());  // { value: 'c', done: false }
console.log(gen.next());  // { value: 'ending', done: true }

我们打印这个实例对象再去.next的执行结果居然是个对象,里面有valuedone两个key

这个value值为a表示的是,执行第一个yield,也就是a,而这个donefalse表示的是整个generator函数还未执行完毕,因此如此往复,执行最后一个就是return出的value值,并且执行完毕

next对应的yield,其done一定为false

function* foo () {
    yield 'a'
    yield 'b'
    yield 'c'
    yield 'ending'
}
let gen = foo() 
console.log(gen.next());  // { value: 'a', done: false }
console.log(gen.next());  // { value: 'b', done: false }
console.log(gen.next());  // { value: 'c', done: false }
console.log(gen.next());  // { value: 'ending', done: false }

所以到这里你其实就明白,next只是给yield下指令,有了next就执行一个yield

如果我们继续让其next,此时已经没有对应的yield让你去执行了,因此不出所料value就是undefined,实际上也果真如此

function* foo () {
    yield 'a'
    yield 'b'
    yield 'c'
    yield 'ending'
}
let gen = foo() 
console.log(gen.next());  // { value: 'a', done: false }
console.log(gen.next());  // { value: 'b', done: false }
console.log(gen.next());  // { value: 'c', done: false }
console.log(gen.next());  // { value: 'ending', done: false }
console.log(gen.next());  // { value: undefined, done: true }

执行generator函数就不是传统意义的执行,它的执行是给我带来一个实例对象(也称之为迭代对象),并且实例对象去.next就是去返回里面的yield,一个next返回一个yielddone表示的是generator是否执行完毕,而return可有可无,return的值就是donetrue的值

既然是实例对象也称之为迭代对象,里面必然是有迭代属性的,可以进行迭代

小试牛刀🌰

function* foo () {
	var o = 1
    yield o++ // 相当于o = o, o = o + 1
    yield o++
    yield o++
}
let gen = foo() 
console.log(gen.next());  // { value: 1, done: false }
console.log(gen.next());  // { value: 2, done: false }
console.log(gen.next());  // { value: 3, done: false }

理清这个之前,我们需要先明白o++++o的区别

o++:先读值后自增

++o:先自增后读值

因此执行第一个yield的时候,返回o的值,就是1,这个时候还有个自增的操作去执行,因此o变成了2,然后执行第二个yield时,就是读值o为2……

改巴改巴

function* foo () {
	var o = 1
    yield ++o // 相当于o = o + 1, o = o 
    yield o++
    yield o++
}
let gen = foo() 
console.log(gen.next());  // { value: 2, done: false }
console.log(gen.next());  // { value: 2, done: false }
console.log(gen.next());  // { value: 3, done: false }

第一个++o就会先自增后读值,yield返回o就是2……

这个东西我改巴改巴

function* foo () {
	var o = 1
    yield o++ 
    yield o++
    yield o++
}
let gen = foo() 
console.log(gen.next());  // { value: 1, done: false }
let gener = foo()
console.log(gener.next()); // { value: 1, done: false }

两个实例对象,互不影响,合乎逻辑!

继续改巴改巴

function* foo () {
	var o = 1
    yield o++ 
    yield 
    yield o++
}
let gen = foo() 
console.log(gen.next());  // { value: 1, done: false }
console.log(gen.next());  // { value: undefined, done: false }
console.log(gen.next());  // { value: 2, done: false }
console.log(gen.next());  // { value: undefined, done: true }

这个也很好理解!执行next就相当于返回yield后面的值,既然yield后面啥也没有,就是undefined,而后面再去读o的值,就第一个o++之后了,因此是2

小试牛刀🌰🌰

function* g() {
    let a = 1
    let b = yield a++ 
    console.log(b); 
    let c = yield a++
}

let gen = g()
console.log(gen.next()); 
console.log(gen.next()); 
// 输出如下:
// { value: 1, done: false }
// undefined
// { value: 2, done: false }

分析下:这里和上面的🌰很不同,因为这里还给yield赋值给别人了,我们其实是不清楚yield赋值是什么样的,很多人会误以为b就是2,因为代码从右往左执行,yield a++对应的第一个next肯定是a = 1,但是这时再去执行a = a + 1,会误会为b就是2,其实yield默认一定是undefinedyield自身的值一定是下一个next里面传的参数

function* g() {
    let a = 1
    let b = yield a++ 
    console.log(b); 
    let c = yield a++
}

let gen = g()
console.log(gen.next()); 
console.log(gen.next(2)); // next的参数,用于指定被我触发的yield的执行结果
// 输出如下:
// { value: 1, done: false }
// 2
// { value: 2, done: false }

第一个yield的值,是下个next传进来的参数

改巴改巴

function* g() {
    let a = 1
    let b = yield a++ 
    console.log(b); 
    let c = yield a++
}

let gen = g()
console.log(gen.next()); 
console.log(gen.next(3)); // next的参数,用于指定被我触发的yield的执行结果
console.log(gen.next(2));
// 输出如下:
// { value: 1, done: false }
// 3
// { value: 2, done: false }
// { value: undefined, done: true }

其实就是来误导你的,本身generator就是两个yield,一个next对应一个yield,因为最后一个next没有相应的yield与之匹配,因此输出undefined,并且执行完毕的状态

继续改巴改巴,这是重点戏

function* g() {
    let a = 1
    let b = yield a++ 
    console.log(b); 
    let c = yield a++
    console.log(a);
    console.log(c);
}

let gen = g()
console.log(gen.next()); 
console.log(gen.next(3)); // next的参数,用于指定被我触发的yield的执行结果
console.log(gen.next(2));
// 输出如下:
// { value: 1, done: false }
// 3
// { value: 2, done: false }
// 3
// 2
// { value: undefined, done: true }

总共就2个yield,这里还有3个next,因此最后一个next一定是undefined,并且执行完毕,既然这里要说generator执行完毕,里面的log打印语句也会顺带执行完毕,因此这个两个打印在两个next之间,是合乎逻辑的,先打印b为3,这个b就是第一个yield的值,而这个yield的值是根据下一个next的参数决定的,如果第二个next没有参数,那么这个b就是undefined,这里传了3,那么就是b = 3,然后再是打印a,到了第二个yield时,已经执行了两个a++了,因此a也是3,最后再是打印c,c就是第二个yield的值,这个值就是第三个next传入的参数,是2,没有问题~

再来一个,这个也很有意思

function* g() {
    yield 1
    yield 2
    yield 3
    yield 4
    yield 5
    return 6
}

for (let val of g()) { // for of就是迭代具有迭代器属性的东西 也就是自带next
    console.log(val); // 1 2 3 4 5 
}

上面早就说了,generator的执行就是一个实例对象,而实例对象也被称之为迭代对象,就是因为自身拥有迭代属性,因此我们可以用for of去遍历它,这个时候肯定有小伙伴们问了,不应该是遍历.next吗,对,没错,这个for of自身就是去迭代拥有迭代属性的东西,迭代的时候自带next

再提一句,for of碰到了return是不会执行的,因此最终输出1 - 5

异步

利用generator去解决异步,最有名的就是thunkco这两个模块了

Generator 函数的异步应用 - ECMAScript 6入门 (ruanyifeng.com)

一个情景,两个定时器,让a先执行,b后执行,能做到这个就相当于解决了异步问题

function a () {
    setTimeout(() => {
        console.log('a');
    }, 1000)
}

function b () {
    setTimeout(() => {
        console.log('b');
    }, 500)
}

thunk

这个写法需要给异步函数传入next,如下

function a (next) {
    setTimeout(() => {
        console.log('a');
        next()
    }, 1000)
}

function b (next) {
    setTimeout(() => {
        console.log('b');
        next()
    }, 500)
}

function c (next) {
    console.log('c');
    next()
}

function* g () {
    yield a
    yield b
    yield c
}

function run (fn) {
    let gen = fn()

    function next(err, data) {
        let result = gen.next(data)
        if (result.done) return 
        result.value(next)
    }

    next()
}

run(g) // a b c

其实阮一峰老师这里写的是结合Promise,如下

function a () {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('a');
            resolve()
        }, 1000)
    })
}

function b () {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('b');
            resolve()
        }, 500)
    })
}

function* g () {
    yield a()
    yield b()
}

let gen = g()
let result = gen.next()

result.value.then(value => {
    gen.next()
})

co

需要安装依赖,这是别人自行封装的

npm init -y // 别忘了初始化
npm i co 

写法如下,写法比thunk优雅,同样需要异步函数内置Promise

var co = require('co')

function a () {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('a');
            resolve()
        }, 1000)
    })
}

function b () {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('b');
            resolve()
        }, 500)
    })
}

function* g () {
    yield a()
    yield b()
}

co(g).then(() => {
    console.log('generator执行完毕');
})

总结

  1. generator可以分段执行,可以暂停
  2. 可以控制每个阶段的返回值
  3. 可以知道是否执行完毕
  4. 可以借助 Thunkco 处理异步,但是写法复杂,所以generator函数的意义就是为了打造asyncawait

最后

generator处理异步其实还不如Promise,毕竟用法上你都需要那些异步函数内置Promise

如果你对春招感兴趣,可以加我的个人微信:Dolphin_Fung,我和我的小伙伴们有个面试群,可以进群讨论你面试过程中遇到的问题,我们一起解决

另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请”点赞+评论+收藏“一键三连,感谢支持!