generator生成器

119 阅读3分钟

先看例子

用生成器生成一个 Fibonacci 数列,并获取其前十个数字:

function * fib(){
    yield 1
    yield 1
    let a = 1
    let b = 1
    let tmp 
    while(true){
        yield a + b
        tmp = a;
        a = b;
        b = tmp + b;
    }
}

const g = fib();

Array.from({length : 10}).forEach(_ => {
    const num = g.next().value
    console.log(num);
})

// 1
// 1
// 2
// 3
// 5
// 8
// 13
// 21
// 34
// 55

生成器中重要的next方法

生成器函数具有惰性执行的特性,每次执行到一个 yield 语句时,生成器会被“冻结“,直到下一个 next 方法被调用:

function * ge(){
    yield 1
    console.log('execute');
    yield 1
}

const g = ge();
g.next() // nothing
g.next() // execute

next 方法会返回一个固定结构的对象: {value : a, done : b}value 表示 yield 抛出的值, done 表示是否所有的 yield 均被执行完成:

function * ge(){
    yield 1 
}

const g = ge();
console.log(g.next()); // { value: 1, done: false }
console.log(g.next());  // { value: undefined, done: true }

next 方法允许我们传入一个参数,这个参数会被作为生成器函数中 yield 的返回值:

function * ge(value){
    while(true){
        const step = yield value ++   
        if(step){
            value += step
        }
    }
}

const g = ge(0);
console.log(g.next().value);  // 0
console.log(g.next(10).value);  // 11
console.log(g.next().value);  // 12

需要注意的是,当我们执行第一个 g.next() 时,生成器中只执行了 yield value ++ 而不是执行了 const step = yield value ++ ,即如果我们期望在调用第一个 next() 时就将 step 设置为10是做不到的:

function * ge(value){
    while(true){
        const step = yield value ++   
        if(step){
            value += step
        }
    }
}

const g = ge(0);
console.log(g.next(10).value); // 0
console.log(g.next().value);  // 1
console.log(g.next().value);  // 2

因为第一次执行时没有执行完整语句 const step = yield value ++ ,所以我们在第一次传入的参数 10 没有被保存起来,它不会进入第二次 next 执行的上下文,因此第一次传入 next 的参数没有任何作用

反观:

function * ge(value){
    while(true){
        const step = yield value ++   
        if(step){
            value += step
        }
    }
}

const g = ge(0);
console.log(g.next().value);  // 0
console.log(g.next(10).value);  // 11
console.log(g.next().value);  // 12

在调用第二个 next 时传递了一个变量 10 ,它会作为前一条 yield 语句的返回值赋值给 step ,从而打印出不同的结果。

手动实现一个 async/await

await 其实可以看作 yield 的一个语法糖,借助生成器的强大功能,我们可以手动实现一个和 async/await

先来看下一个传统的 async/await 语法:

function getData() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(1)
        }, 2000)
    })
}

const req = async () => {
    const res = await getData()
    console.log({ res });
    const res2 = await getData()
    console.log({ res2 });
}

async 其实起到的是包装并执行的作用,它会转换函数为生成器并执行它,类似于如下这样:

function* req() {
    const res = yield getData()
    console.log({ res });
    const res2 = yield getData()
    console.log({ res2 });
}

executer(req)

我们用 executer 函数执行生成器,将执行的结果通过 next 函数传递给 res , res2 对象,从而实现 async/await 语句:

function executer(generater) {
    const g = generater()
    const g_status = g.next() // 执行 yield getData()
    while (!g_status.done) { 
        const p = g_status.value // getData()返回Promise对象
        p.then(res => {
            g.next(res) // 将前一次的执行结果作为 yield 的返回值传递回生成器
        })
    }
}

当我们信心满满的运行上面的代码时,会发生程序陷入了死循环,因为异步方法 .then 想要执行时被 while 语句阻塞了,因此我们不能使用 while 语句,使用递归是一个更好的做法:

function executer(generater) {
    const g = generater()
    const gStatus = g.next()

    function exec(status){
        if(status.done){ // done为true,所有yield执行完成
            return 
        }
        const p = status.value // Promise对象
        p.then(res => {
            const nextStatus = g.next(res) // 将前一次的执行结果作为 yield 的返回值传递回生成器,返回执行完 next 后的生成器状态
            exec(nextStatus) // 将生成器状态传递给下一个递归函数,如果done为 true ,则终止执行,否则继续调用下一个next
        })
    }

    exec(gStatus)
}

使用我们写的 async/await

function getData() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(1)
        }, 2000)
    })
}
function* req() {
    const res = yield getData()
    console.log({ res });
    const res2 = yield getData()
    console.log({ res2 });
}


function executer(generater) {
    const g = generater()
    const gStatus = g.next()

    function exec(status){
        if(status.done){
            return 
        }
        const p = status.value
        p.then(res => {
            const nextStatus = g.next(res)
            exec(nextStatus)
        })
    }

    exec(gStatus)
}

executer(req)

// { res: 1 }
// { res2: 1 }