JS Generator

364 阅读4分钟

JS中的Generator有两种类型,一是Generator Function,二是Generator Object。

Generator Function

语法

中文翻译为“生成器函数”,特点是在执行时能暂停,后面又能从暂停处继续执行。

demo

// 1. 创建生成器函数
function* gen() {
    const now = Date.now();
    // 2. yield左侧收到的数据
    const receiveTime = yield now;
    return receiveTime - now;
}
// 原型
console.log(gen.__proto__);
// 3. 函数生成的对象
const generator = gen();
// 4. 调用对象的next方法
const result1 = generator.next();
console.log('result1:', result1);

setTimeout(() => {
    console.log('\n generator 第二次next调用');
    // 5. 第二次调用next,并传参
    const result2 = generator.next(Date.now());
    console.log('result2:', result2);
}, 300);
/*
    GeneratorFunction {}
    result1: { value: 1659254017742, done: false }

     generator 第二次next调用
    result2: { value: 302, done: true }
*/

函数执行说明

  1. 调用生成器函数,并不会马上执行它内部的语句,而是返回一个Generator对象(后文会提到)
  2. 第一次调用next方法,函数中的语句会执行到第一个的一个yield位置为止,yield后紧跟iterator要返回的值
  3. 下一次调用next方法,从暂停的yield处继续向下执行,直到遇到下一个yield表达式
  4. 如果没有遇到yield表达式,则一直运行函数,直到遇到return,return语句的值则会作为返回值。如果没有return语句,则返回值为undefined

next

  1. next方法返回一个对象

    {
        value: any, // 上面提到的返回值
        done: boolean, // 生成器函数是否已经执行完毕并返回
    }
    
  2. next方法,如果传入了参数,那个这个参数会传给上一条执行的yield语句左边的变量,比如上面demo的第5处

yield

  1. yield语句后直接跟值,则此值会作为next返回对象的value属性值,比如demo第
  2. yield* 将控制权交给另外一个生成器函数
    function* userGen() {
        const user = {name: '橘子', age: '18'};
        yield user.name;
        yield user.age;
    }
    function* logUser() {
        const id = 888;
        yield id;
        yield* userGen(); // yield*
        yield 'end';
    }
    const iterator = logUser();
    console.log('1 ' ,iterator.next());
    console.log('2 ' ,iterator.next());
    console.log('3 ' ,iterator.next());
    console.log('4 ' ,iterator.next());
    console.log('5 ' ,iterator.next());
    /* 运行结果
    1  { value: 888, done: false }
    2  { value: '橘子', done: false }
    3  { value: '18', done: false }
    4  { value: 'end', done: false }
    5  { value: undefined, done: true }
    */
    

ES5实现

Generator Function是ES2015的新特性,它在低版本浏览器是如何支持的?如果用ES5来实现此函数?
测试函数

    function* userGen() {
        const user = {name: '橘子', age: '18'};
        yield user.name;
        yield user.age;
    }
    const iterator = userGen();
    iterator.next();

babel转换

以数userGen为例,查看babel的转换结果

Xnip2022-07-31_16-02-38.jpg

  1. _regeneratorRuntime().mark函数的作用是?

Xnip2022-07-31_16-23-31.jpg 2. _regeneratorRuntime().wrap函数的作用是? Xnip2022-07-31_16-21-16.jpg 3. context对象的作用? Xnip2022-07-31_16-39-11.jpg 4. 更详细的解析可参考冴羽的ES6 系列之 Babel 将 Generator 编译成了什么样子

简易版本

babel的代码全面且复杂,但可以从中抽出关键点来实现最简易的运行版本

function userGen() {
    var user;
    // 1. 使用自定义的wrap函数
    return wrap(function userGen$(_context) {
        while (1) {
            switch ((_context.prev = _context.next)) {
            case 0:
                user = {name: "橘子",age: "18"};
                _context.next = 3;
                return user.name;
            case 3:
                _context.next = 5;
                return user.age;
            case 5:
            case "end":
                return _context.stop();
            }
        }
    });
}
// 2. 自定义wrap,返回一个带有next方法的对象
function wrap(genFun) {
    // genFun函数执行上下文
    const context = {
        prev: 0,
        next: 0,
        done: false,
        stop: function() {
            this.done = true;
        }
    };
    return {
        next: function() {
            // 返回结构 value, done
            // genFun调用会修改context的next和prev值
            return { value: genFun(context), done: context.done };
        }
    }
}
// 得到(类)生成器对象 & next方法调用
const iterator = userGen();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
/*
    { value: '橘子', done: false }
    { value: '18', done: false }
    { value: undefined, done: true }
*/

Generator Object

原型

上文我们提到了Generator Function会返回一个Generator对象,它有next方法。 此对象在JS中的原型为Generator。原型上除了next方法外,还有两个方法。

  1. Generator.prototype.return 返回给定的值并结束生成器
    function* userGen() {
        const user = {name: '橘子', age: '18'};
        yield user.name;
        yield user.age;
    }
    const iterator = userGen();
    console.log(iterator.__proto__);
    
    console.log(iterator.return('直接结束'));
    console.log(iterator.next());
    /*
        Object [Generator] {}
        { value: '直接结束', done: true }
        { value: undefined, done: true }
    */
    
  2. Generator.prototype.throw 向生成器抛出一个错误
    • throw方法被生成器函数内部捕获之后,会执行下一条可执行的yield表达式,和调用一次 next效果相同
    • throw 方法也有返回值
    function* userGen() {
        const user = {name: '橘子', age: '18'};
        try{
            yield user.name;
            yield console.log('user name: ' + user.name);
        }catch(e){
            console.log('函数内部捕获错误:', e.message);
        }
        yield console.log('user age: ' + user.age);
        yield 'end';
    }
    const iterator = userGen();
    iterator.next();
    
    console.log(iterator.throw(new Error('这个是错误')));
    /* 执行结果为:
        函数内部捕获错误: 这个是错误
        user age: 18
        { value: undefined, done: false }
    */
    
    • 如果throw方法执行时,next方法一次都没有执行过,会在函数外抛出错误(此时生成器函数还未开始执行,只能在函数外部抛出)
    function* userGen() {
          // 同上
    }
    
    const iterator = userGen();
    try{
        console.log(iterator.throw(new Error('这个是错误')));
    }catch(e){
        console.log('全局捕获错误: ', e.message);
        console.log(iterator.next());
    }
    /*执行结果为:
        全局捕获错误:  这个是错误
        { value: undefined, done: true }
    */
    

参考文章

generator 阮一峰