Generator函数

Generator函数是ES6的异步编程解决方案,它是一个状态机,封装了多个内部状态。yield语句是暂停执行的标记,next方法使得Generator函数分段执行。Generator可以用于异步操作的同步化表达、控制流管理、部署Iterator接口以及作为数据结构使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介

1、Generator函数是ES6提供的一种异步编程方案
2、从语法上,首先可以把它理解成一个状态机,封装了多个内部状态
3、Generator函数还是一个遍历器对象生成函数,返回遍历器对象
4、Generator函数有两个特征:1、function命令与函数名之间有一个星号;2、函数体内部使用yield语句定义不同的内部状态(“yield”在英语里的意思是“产出”)

function* helloWorldGenerator(){
    yield 'hello';
    yield 'world';
    return 'ending';
}
var hw = helloWorldGenerator();

定义了一个Generator函数——helloWorldGenerator,它内部有两个yield语句“hello”和“world”,即该函数有三个状态:hello、world和return语句(结束执行)
在调用Generator函数后,该函数并不执行返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象
下一步必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或从函数上一次停下来的地方开始执行,直到遇到下一条yield语句(或return语句)为止。换言之,Generator函数是分段执行的,yield语句是暂停执行的标记,而next方法可以恢复执行

hw.next() // {value: 'hello', done: false }
hw.next() // {value: 'world', done: false }
hw.next() // {value: 'ending', done: true }
hw.next() // {value: undefined, done: true }

ES6没有规定function关键字与函数名之间的星号必须在哪

yield语句

由于Generator函数返回的遍历器对象只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield语句就是暂停标志。
遍历器对象的next方法的运行逻辑如下:

1、遇到yield语句就暂停执行后面的操作,并将紧跟在yield后的表达式的值作为返回的对象的value属性值
2、下一次调用next方法时才会继续执行,直到遇到下一个yield语句。
3、如果没有再遇到新的yield语句,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值作为返回的对象的value属性值。
4、如果该函数没有return语句,则返回的对象的value属性值为undefined。

要注意的是,yield语句后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为JS提供了手动的“惰性求值”的语法功能。

function* gen(){
    yield 123+456;
}

上面的代码中,yield后面的表达式123+456不会立即求值,只会在next方法将指针移到这一语句时才求值

Generator函数可以不用yield语句,这时就变成了一个单纯的暂缓执行函数。

function* f(){
    console.log('执行了');
}
var generator = f();
setTimeout(function(){
    generator.next();
}, 2000);

如果f只是普通函数,在为变量generator赋值时就会执行。但是函数f是一个Generator函数,于是就变成只有调用next方法时才会执行。
要注意yield语句不能再普通函数中,yield语句在表达式中,必须放在括号里

next方法

next方法可以携带一个参数:

function* f(){
    for(var i=0; true; i++){
        var reset = yield i;
        if(reset) {i = -1};
    }
}
var g = f();
g.next() // {value: 0, done: false}
g.next() // {value: 1, done: false}
g.next(true) // {value: 0, done: false}

定义了一个可以无限循环的Generator函数f,如果next方法没有参数,每次运行到yield语句,变量reset就会被重置为这个参数,因而i会等于-1,下一轮循环就从-1开始递增。

Generator函数从暂停到恢复运行,其上下文状态是不变的。通过next方法的参数就有办法在Generator函数开始运行后继续向函数提内部注入值。也就是说,可以在Generator函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。

function* foo(x){
    var y = 2 * (yield(x + 1));
    var z = yield( y / 3 );
    return (x + y + z);
}
var a = foo(5);
a.next()//Object{value:6, done:false}
a.next()//Object{value:NaN, done:false}
a.next()//Object{value:NaN, done:false}

var b = foo(5);
b.next()//Object{value:6, done:false}
b.next(12)//Object{value:8, done:false}
b.next(13)//Object{value:42, done:false}

如果next中没有参数,yield就返回undefined。所以在a第二次调next是y=2*undefined,所以结果为NaN。
在实例b中,第一次yield返回6。第二次传入12,所以第二次返回的值为2*12再除以3,所以结果为8.第三次同理
next参数就是上一条yield的返回值,所以第一次yield时不能带有参数。V8引擎会忽略第一次next时的参数,只从第二次使用next方法开始参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。如果想要第一次next带有参数,需要在Generator函数外再包一层。

function wrapper(generatorFunction){
    return function(...args){
        let generatorObject = generatorFunction(...args);
        generatorObject.next();
        return generatorObject;
    };
}
const wrapped = wrapper(function* () {
    console.log('First input: ${yield}');
    return 'DONE';
});
wrapped().next('hello!');

如果Generator函数不用wrapper先包一层,是无法第一次调用next方法就输入参数的。

for…of

for…of循环可以自动遍历Generator函数,且此时不再需要调用next方法。

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

for(let v of foo(){
    console.log(v);
})
//1 2 3 4 5

for…of遇到done属性为true就会终止,且不包含该返回对象。所以return的6就不包括在for…of中

Generator与协程

协程是一种程序运行的方式,可以理解成“协作的线程”或“协作的函数”。协程可以用单线程实现,也可以用多线程实现;前者是一种特殊的子线程,后者是一种特殊的线程。

协程与子例程的差异

传统的“子例程”采用堆栈式“先进后出”的概念实现,只有当调用的子函数完全执行完毕,才会结束执行父函数。协程与其不同,多个线程可以并行执行,但只有一个线程处于正在运行状态,其他线程都处于暂停态,线程之间可以交换执行权。也就是说,一个线程执行到一半,可以暂停执行,将执行权交给另一个线程,等到稍后回收执行权时再恢复执行。这种可以并行执行,将执行权交给另一个线程,等到稍后收回执行权时再恢复执行。这种可以并行执行、交换执行权的线程,就成为协程。
从实现上看,在内存中子例程只使用一个栈,而协程是同时存在多个栈,但只有一个栈是在运行态。也就是说,协程是以多占用内存为代价实现多任务的并行运行。

协程与普通线程的差异

不难看出,协程适用于多任务运行的环境。在这个意义上,它与普通的线程很相似,都有自己执行的上下文,可以分享全局变量。他们不同之处在于,同一时间可以有多个线程是抢占式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。
ECMAScript是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误时可以找到原始的调用栈,不至于像异步操作的回调函数那样,一旦出错原始的调用栈早已结束。
Generator函数是ES6对协程的实现,但属于不完全实现。Generator函数被称为“半协程”,意思是只有Generator函数的调用者才能将程序的执行权还给Generator函数。如果是完全实现的协程,任何函数都可以让暂停的协程继续执行。
如果将Generator函数当作协程,完全可以将多个需要互相协作的任务写成Generator函数,它们之间使用yield语句交换控制权。

应用

Generator可以暂停函数执行,返回任意表达式的值。这种特点使得Generator有多种应用场景。

1、异步操作的同步化表达

Generator函数的暂停执行效果,意味着可以把异步操作卸载yield语句里面,等到调用next方法时再执行。所以,Generator函数的一个重要实际意义就是用于处理异步操作,改写回调函数

function* loadUI () {
    showLoadingScreen();
    yield loadUIDataAsynchronously();
    hideLoadingScreen();
}
var loader = loadUI();
//加载UI
loader.next();
//卸载UI
loader.next();

上面的代码表示,第一次调用loadUI函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用next方法,则会显示加载界面,并且异步加载数据。等到数据加载完成,再一次使用next方法,则会隐藏加载界面。可以看到,这种写法的好处是所有加载界面的逻辑都被封装在一个函数中,按部就班非常清晰。

Ajax是典型的异步操作,通过Generator函数部署Ajax操作,可以用同步的方式表达。

function* main(){
    var result = yield request("http://xxoo");
    var resp = JSON.parse(result);
    console.log(resp.value);
}
function request(url){
    makeAjaxCall(url, function(response){
        it.next(response);
    });
}
var it = main();
it.next();

上面的main函数就是通过AJAX操作获取数据。可以看到,除了多了一个yield,它几乎与同步操作的写法一模一样。注意,makeAjaxCall函数中的next方法必须加上response参数,因为yield语句构成的表达式本身是没有值的,总是等于undefined。

通过Generator实现的逐行读取文本文件。

function* numbers(){
    let file = new FileReader("numbers.txt");
    try{
        while(!file.eof){
            yield parseInt(file.readLine(), 10);
        }
    }finally{
        file.close();
    }
}

上面代码可以使用yield手动逐行读取文件

2、控制流管理

如果有一个多步操作非常耗时,采用回调函数可能会写成这样。。

step1(function(value1){
    step2(value1, function(value2){
        step3(value2, function(value3){
            step4(value3, function(value4){
                // Do something with value4
            })
        })
    })
});

采用Promise改写上面的代码如下

Q.fcall(step1)
  .then(step2)
  .then(step3)
  .then(step4)
  .then(function(value4){
        //Do something with value4
  }, function (error) {
        // Handle any error from step1 through step4
  })
  .done();

上面代码把回调函数改成了直线执行的形式,但是加入了大量Promise的语法
Generator函数可以进一步改善代码运行流程

function* longRunningTask(){
    try {
        var value1 = yield step1();
        var value2 = yield step2(value1);
        var value3 = yield step2(value2);
        var value4 = yield step2(value3);
        // do something with value4
    }catch(e){
        // handle any err
    }
}

然后使用一个函数按次序自动执行所有步骤

scheduler(longRunningTask());

function scheduler(task){
    setTimeout(function(){
        var taskObj = task.next(task.value);
        // 如果Generator函数未结束,就继续调用
        if(!taskObj.done){
            task.value = taskObj.value
            scheduler(task);
        }
    }, 0);
}

yield语句是同步运行,不是异步运行(否则失去取代回调函数的设计目的)
多个任务按顺序一个接一个执行时,yield语句可以按顺序排列。多个任务需要并列执行时(比如只有当任务A和任务B都执行完时才能执行C),可以采用数组的写法。

function* parallelDownloads(){
    let [ text1, text2 ] = yield [
        taskA();
        taskB();
    ];
    console.log(text1, text2);
}

上面的代码中,yield语句的参数是一个数组,成员就是两个任务——taskA和taskB,只有等这两个任务都完成,才会接着执行下面的语句

3、部署Iterator接口

利用Generator函数可以在任意对象上部署Iterator接口。

function* iterEntries(obj){
    let keys = Object.keys(obj);
    for(let i=0; i<keys.length; i++){
        let key = key[i];
        yield [key, obj[key]];
    }
}
let myObj = {foo: 3, bar: 7};
for(let [key, value] of iterEntries(myObj)){
    console.log(key, value);
}
// foo 3  bar 7

代码中,myObj是一个普通对象,通过iterEntries函数就有了Iterator接口。也就是说,可以在任意对象上部署next方法。

4、作为数据结构

Generator可以看作数据结构,因为Generator函数可以返回一系列的值,这意味着它可以对任意表达式提供类似数组的接口。

function *doStuff(){
    yield fs.readFile.bind(null, 'xx.txt');
    yield fs.readFile.bind(null, 'oo.txt');
    yield fs.readFile.bind(null, 'zz.txt');
}

上面代码一次返回三个函数,但是由于使用了Generator函数,导致可以像处理数组那样处理这三个返回的函数

for(task of doStuff()){
    //task是一个函数,可以像回调函数那样使用它
}

Generator使得数据或者操作具备了类似数组的接口

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值