【ES6标准入门】JavaScript中的Generator函数的使用和细节,异步操作思路之一(初级)

在这里插入图片描述

😁 作者简介:一名大三的学生,致力学习前端开发技术
⭐️个人主页:夜宵饽饽的主页
❔ 系列专栏:JavaScript进阶指南
👐学习格言:成功不是终点,失败也并非末日,最重要的是继续前进的勇气

​🔥​前言:

这里是关于异步操作之一的知识点Generator函数的使用和理解。 这是我自己的学习JavaScript的笔记,希望可以帮助到大家,欢迎大家的补充和纠正

🌻​ 理解异步操作的中级篇在下一节:JavaScript的Generator函数的异步应用,异步操作思路之一(中级)

第16章 Generator函数的语法

16.1 简介

16.1.1 基本概念

Generator函数是ES6提供的一种异步编程的解决方案,语法行为与传统函数完全不同,我们可以从多个角度理解

  1. 从语法上:可以把它理解成一个状态机,封装了多个内部状态
  2. 从形式上:其是一个普通函数,但是有两个特征:一是function命令与函数名之间有一个星号;二是函数体内部使用yield语句定义不同的内部状态
  3. 从返回结果上:其调用方法虽然与普通函数一样,但是调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象
  4. 从执行上:Generator函数是分段执行的,yield语句是暂停执行的标记,而next方法可以恢复执行

实例代码:

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

var hw=helloWorldGenerator()

var hw1=hw.next()
console.log(hw1)
//{ value: 'hello', done: false }


var hw2=hw.next()
console.log(hw2)
//{ value: 'world', done: false }


var hw3=hw.next()
console.log(hw3)
//{ value: 'ending', done: true }

var hw4=hw.next()
console.log(hw4)
//{ value: undefined, done: true }

上面的代码一共调用了4次next方法

  1. 第一次调用,Generator函数开始执行,直到遇到第一条yield语句为止,next方法返回一个对象,它的value属性的值解释当前yield语句的值,而done属性的值false表示遍历还没有结束
  2. 第二次调用,Generator函数从上次yield语句停下的地方,一直执行到下一条yield语句,如此反复调用执行
  3. 第三次调用,此时遇到return语句(如果没有return语句,就执行到函数结束)。next方法返回的对象的value属性的值解释return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined,done属性的值true表示遍历已经结束)
  4. 第四次调用:此时Generator函数已经运行完毕,next方法返回的对象的value属性为undefined,done属性为true,以后再调用next方法,返回的都是这个值

总结一下:调用Generator函数返回一个遍历器对象,代表Generator函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield语句后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束

📝 补充一点:关于function关键字和函数名之间的星号写在哪个位置,下面的四种写法都是可以的

function * foo(x,y){}
function *foo(x,y){}
function* foo(x,y){}
function*foo(x,y){}

由于Generator函数依旧是普通函数,所以一般的写法是上面的第三种

16.1.2 yield表达式

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

遍历器的对象的next方法的运行逻辑如下:

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

❗️ 注意:只有调用next方法且内部指针指向该语句时才会执行yield语句后面的表达式,因此等于为js提供了手动的惰性求值的语法功能

function* gen(){
	return 123+789
}

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

yield和return的语句的异同点:

相同点:都可以返回紧跟在语句后的表达式的值

不同点:

  • 遇到yield函数暂停执行,下一次会从该位置继续向后执行,一个函数可以有多条yield语句
  • 遇到return语句不会具备位置记忆功能,一个函数只能有一个return

📝 使用细节:

  1. yield表达式只能用在Generator函数里面,用在其他地方会报错

    //易错的案例
    var arr=[1,[[2,3],4],[5,6]]
    
    var flat=function* (a){
        a.forEach(item => {
            if(typeof item !== 'number'){
                yield* flat(item)
            } else{
                yield item
            }   
        });
    }
    
    for(var f of flat(arr)){
        console.log(f)
    }
    

    上述的代码中,因为forEach方法的参数时一个普通函数,但是在里面使用yield表达式

  2. yield表达式如果用另一个表达式之中,必须放在圆括号里面

    function* demo(){
        console.log('Hello'+yield) //SyntaxError
        console.log('Hello'+(yield)) //OK
    }
    
  3. yield表达式用作函数参数或放在表达式的右边可以不加括号

    function* demo(){
    foo(yield 'a',yield 'b')
    let input=yield
    }
    
16.1.3 与Iterator接口的关系

由于Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的Symbol,iterator属性,从而使得该对象具有Iterator接口

var myIterable={}

myIterable[Symbol.iterator]=function* (){
    yield 1;
    yield 2yield 3
}

[...myIterable] //[1,2,3]

上面的代码中,Generator函数赋值给Symbol.iterator属性,从而使得myIterable对象具有Iterator接口

Generator函数执行后,返回一个遍历器对象,该对象本身也具有Symbol.iterator属性,执行后返回自身

function* gen(){
// some code
}

var g=gen()

g[Symbol.iterator]()===g
//true

上面的代码中,gen是一个Generator函数,调用它会生成一个遍历器对象g。它的Symbol.iterator属性也是一个遍历器对象生成函数,执行后返回自己

16.2 next方法的参数

yieldd语句本身是没有返回值,或者说总是返回undefined。next方法可以带有一个参数,该参数会被当作为上一条yield语句的返回值

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

var a=foo(5)
console.log(a.next());
console.log(a.next());
console.log(a.next());

var b=foo(5)
console.log(b.next());
console.log(b.next(12));
console.log(b.next(13));

上面的代码中,与正常的yield语句不同的是,每一层的yield语句都是有依靠关系的,下一层的yield语句的表达式中,依靠上一层yield语句的返回值,所以这种情况下next中传递参数显得尤为重要

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

❗️ 注意:由于next方法的参数表示上一条yield语句的返回值,所以第一次使用next方法时传递的参数时无效,V8引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的,从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数

⭐️ 下面是一个直观的特别的调用例子:

function* dataConsumer(){
    console.log('Started')
    console.log(`1. ${yield}`)
    console.log(`2. ${yield}`)
    return 'result'
}

let genObj=dataConsumer()

genObj.next()
//Started
genObj.next('a')
//1. a
genObj.next('b')
//2. b

16.3 for…of循环

for…of循环可以自动遍历Generator函数生成的Iterator对象,且此时不再需要调用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

上面的代码使用fot…of循环依次显示5条语句的值

❗️ 注意**:一旦next方法的返回对象的done属性为true,for…of循环就会终止,且不包含该返回对象,所以上面的return语句返回的6不包括在for…of循环中**

📖 关于原生js对象没有遍历接口,无法使用for…of循环,这里可以使用Generator函数两种思路

  • 通过Generator函数为它加上这个接口后就可以使用
  • 将Generator函数加到对象的SYmbol.iterator属性上

使用细节:

  1. 除了for…of循环,扩展运算符(…),解构赋值和Array.from方法内部调用的都是遍历器接口,这意味着,它们都可以将Generator函数返回的Iterator对象作为参数

16.4 Generator.prototypr.throw()

Generator函数返回的遍历器对象都有一个throw方法,可以在函数体外抛出错误,然后再Generator函数体内捕获

var g=function* (){
    try {
        yield;
    } catch (e) {
        console.log('内部捕获',e)
    }
}

var i=g()
i.next();
try{
    i.throw('a')
    i.throw('b')
}catch(e){
    console.log('外部捕获',e)
}

上面的代码中,遍历器对象i连续抛出两个错误,第一个错误被Generator函数体内的catch语句捕获。i第二次抛出错误,由于Generator函数内部的catch语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了Generator函数体,被函数体外的catch语句捕获。

使用细节:

  1. throw方法可以接受一个参数,该参数会被catch语句接收,建议抛出Error对象的实例
  2. 遍历器对象的throw方法和全局的throw命令是不一样的,是无关的,两种互相不影响,上面例子的错误是用遍历器对象throw方法抛出的,而不是全局的throw命令,使用throw命令抛出的错误只能被函数体外的catch捕获
  3. 如果Generator函数体内没有部署try…catch代码块,那么throw方法抛出的错误将会被外部try…catch代码块捕获
  4. 如果Generator函数体内部署了try…catch代码块,那么遍历器的throw方法抛出的错误不影响下一次遍历,否则遍历直接终止
  5. throw方法被捕获以后会附带执行下一条yield表达式,即附带执行一次next方法

🤔 问题:这种函数体内捕获错误的机制的益处是什么?

😄 答:这种函数函数体内捕获错误的机制大大方便了对错误的处理,对于多个yield表达式,可以只用一个try…catch代码块来捕获错误,如果使用回调函数的写法想要捕获多个错误,就不得不每个函数写一个错误处理语句,而现在只再Generator函数内部写一次catch语句就可以了

Generator函数体外抛出的错误可以在函数体内捕获,反过来,Generator函数体内抛出的错误也可以被函数体外的catch捕获

function* foo(){
    var x=yield 3;
    var y=x.toUpperCase()
    yield y;
}

var it=foo()
console.log(it.next())

try {
    it.next(42)
} catch (e) {
    console.log(e)
}

这个和在外部抛出,内部捕获错误的不同点就是,一旦Generator执行的过程中抛出错误,就不会再执行下去了,如果此后还调用next方法,将返回一个value属性等于undefined,done属性等于true的对象,即js引擎认为这个Generator已经结束运行。

👨 总结一下

我们可以把抛出错误分为三种情况:全局throw,外部it.throw(),内部的错误

我们可以把捕获错误分为两种情况:内部捕获,外部捕获

可以确定出思维图:先后表示执行顺序

在这里插入图片描述

16.5 Generator.prototype.return()

Generator函数返回的遍历器对象还有一个return方法,可以返回给定的值,并终结Generator函数的遍历

function* gen(){
    yield 1;
    yield 2;
    yield 3;
}

var g=gen()

g.next() 		//{value:1,done:false}
g.return('foo') //{value:'foo',done"true}
g.next() 		//{value:undefined,done:true}

📝 使用细节:

  1. 如果return方法调用时不提供参数,则返回值的value属性为undefined
  2. 如果Generator函数内部有try…finally代码块,那么return方法会推迟到finally代码块执行完再执行

16.6 yield*表达式

如果在Generator函数内部调用另一个Generator函数,默认情况下是没有效果

不过我们可以调用yield*语句,用来在一个Generator函数里面执行另一个Generator函数

function* foo(){
    yield 'a';
    yield 'b';
}

function* bar(){
    yield 'x';
    foo()
    yield 'y'
}

for(let v of bar()){
    console.log(v);
}
//x
//y

//使用yield*调用Generator函数
function* bar1(){
    yield 'x';
    yield* foo();
    yield 'y';
}

//等同于
function* bar(){
    yield 'x';
    yield 'a';
    yield 'b';
    yield 'y';
}

//等同于
function* bar(){
    yield 'x';
    for(let v of foo()){
        yield v
    }
    yield 'y'
}

for(let v of bar1()){
    console.log(v)
}
//x
//a
//b
//y

从语法的角度看,如果yield命令后面跟一个遍历器对象,那么想要在yield命令后面加上星号,表明返回的是一个遍历器对象,这被称为yield*语句

📝 使用细节:

1. yield*后面的Generator函数在有没有return语句时会表现出不同的语法行为

  • 没有return语句时,等同于在Generator函数内部部署一个for…of循环,简单来说yield*就是for…of的简介形式
  • 有return语句时,则需要用var value=yield* iterator的形式获取return语句的值

2.任何数据结构只要有Iterator接口,就可以被yield*遍历

🌵 实例:

1. yield命令可以很方便地取出来嵌套数组的所有成员

function* iterTree(tree){
        if(Array.isArray(tree)){
            for(let i=0;i<tree.length;i++){
                yield* iterTree(tree[i])
            }
        }else{
            yield tree
        }
}

const tree=['a',['b','c'],['d','e']]
for(let x of iterTree(tree)){
    console.log(x)
}

2.使用yield语句完成遍历二叉树

16.7 作为对象属性的Generator函数

如果一个对象的属性是Generator函数,那么可以简写成下面的形式

let obj={
	*muGeneratorMethod(){

	}
}

16.8 Generator函数this

Generator函数总是返回一个遍历器,ES6规定这个遍历器是Generator函数的实例,它也继承了Generator函数的prototype对象上的方法

function* g(){}

g.prototype.hello=function(){
        return 'hi'
};

let obj=g()

console.log(obj instanceof g) //true
console.log(obj.hello()) //'hi

上面的代码表明,Generator函数g返回遍历器obj是g的实例,而且继承g.prototypr。但是,如果把g当作普通的构造函数,则不会生效,因为g的返回值总是遍历器对象,而不是this对象

function* g(){
this.a=11
}
let obj=g()
obj.a //undefined

📝 使用细节:

1. Generator函数不能跟new命令一起使用,否则会报错

🤔 问题:那么有没有办法可以让Generator函数返回一个正常的对象实例,即可以用next方法,又可以获得正常的this呢

😄 答:有两种方案:

  • 首先生成一个对象,使用call方法绑定Generator函数内部的this,这样,构造函数调用以后,这个空对象就是Generator函数的实例对象

    function* f(){
    this.a=1;
    yield this.b=2
    yield this.c=3
    }
    
    var obj={}
    var f=F.call(obj)
    f.next() //Object{value:2,done:false}
    f.next() //Object{value:3,done:false}
    f.next() //Object{value:undefined,done:false}
    
    obj.a //1
    obj.a //2
    obj.c //3
    
  • 将上述代码中的obj换成F.prototype

16.9 含义

16.9.1 Generator与状态机

Gnerator是实现状态机的最佳结构,比如下面的clock函数就是一个状态机

var ticking=true
var clock=function(){
    if(ticking){
        console.log('Tick!')
    }else{
        console.log('Yock!')
    }
    ticking=!ticking
}

上面的clock函数一共有两种状态(Tick和Tock),每运行一次,就改变一次状态,这个函数如果用Generator实现,代码如下

var clock=function* (){
    while(true){
        console.log('Tick!')
        yield;
        console.log('Tock!')
        yield;
    }
}

对比上面的Generator实现与ES5实现,可以看到少了用来保存状态的外部变量ticking,这样就更加简洁,更安全(状态不会被非法篡改),更符合函数式编程的思想

16.9.2 Generatoe协程

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

协程与子例程的差异:

​ 传统的“子例程”(subroutine)采用堆栈式“后进先出”的执行方式,只有当调用的子数完全执行完毕,才会结束执行父函数。协程与其不同,多个线程(单线程情况下即多个函数)可以并行执行,但只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)都处于暂停态(suspended),线程(或函数)之间可以交换执行权。也就是说,一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权时再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程。

​ 从实现上看,在内存中子例程只使用一个栈(stack),而协程是同时存在多个栈,但只有一个栈是在运行态。也就是说,协程是以多占用内存为代价实现多任务的并行运行

协程与普通线程的差异

​ 不难看出,协程适用于多任务运行的环境。在这个意义上,它与普通的线程很相似,都有自己的执行上下文,可以分享全局变量。它们的不同之处在于,同一时间可以有多个线程处于运行态,但是运行的协程只能有一个,其他协程都处于暂停态。此外,普通的线程是抢占式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。

​ 由于JavaScript是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误的时候,可以找到原始的调用栈。不至于异步操作的回调函数那样,一旦出错原始的调用栈早就结束。

​ Generator函数是ES6对协程的实现,但属于不完全实现。Generator函数被称为“半协程”,意思是只有Generator函数的调用者才能将程序的执行权还给Generator函数,如果是完成实现的协程,任何函数都可以让暂停的协程继续执行

​ 如果将Generator函数当作协程,完全可以将多个需要互相协作的任务写成Generator函数,它们之间使用yield语句交换控制权

16.10 应用

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

16.10.1 异步操作的同步化表达

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

function* loadUI(){
	showLoadingScreen()
	yield loadUIDataAsynchronously()
    hideLoadingScreen()
}

var loader=loadUI()
//加载UI
loader.next()

//卸载UI
loader.next()

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

16.10.2 控制流管理
function *longRunningTask(value1){
    tyr{
		var value2=yield step1(value1)
        var value3=yield step1(value2)
        var value4=yield step1(value3)
        var value5=yield step1(value4)
        
    }catch{
        //Handle any error from step1 through step4
    }
}

scheduler(longRunningTask(initialValue));
function scheduler(task){
	var taskObj=task.next(task.value)
    if(!taskObj.done){
		task.value=taskObj.value
        scheduler(task)
    }
}

❗️ 注意:上面的这种做法只适合同步操作,即所有的task都必须同步,不能有异步,因为这里的代理一得到返回值就继续往下执行,没有判断异步操作何时完成。如果要控制异步操作流程,详见后文关于异步操作的内容

16.10.3 部署Iterator接口

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

function* iterEntries(obj){
    let keys=Object.keys(obj)
    for(let i=0;i<keys.length;i++){
        let key=keys[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
16.10.4 作为数据结构

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值