当前位置: 首页 > Web前端 > HTML5

JS 用状态机的思想看Generator之基本语法篇

时间:2023-04-05 22:55:15 HTML5

JS用状态机的思想看Generator的基本语法同时在SegmentFault问答区看到有前端小伙伴对Generator的语法和执行过程有一些疑问,所以想分享一下我对Generator的理解,希望对前端社区有所帮助。Generator的本质Generator的本质是一个状态机。yield关键字的作用是拆分两个状态。右边的语句在前一个状态下执行,而左边的语句要在下一个状态下执行。如果右侧为空,则默认为undefined,如果左侧为空,则默认为赋值语句,被赋值的变量永远不会被调用。当调用Generator函数获取迭代器时,状态机处于初始状态。迭代器调用next方法后,跳转到下一个状态,然后执行该状态的代码。当遇到return或者最后的yield时,就进入final状态。最终状态的标识是next方法返回的对象的done属性。Generator状态跳转Generator函数执行后,会生成一个迭代器,迭代器包含3个主要方法:next、throw和return。它们的本质是改变状态机的状态,但是throw和return是强制改变,next是按照定义好的流程去改变。下面我分别说一下这三种方法。下一个方法查看以下示例:function*gen(){console.log("state1");让state1=yield"state1";console.log("state2");让state2=yield"state2";安慰。log("end");}我们声明了一个名为gen的Generator函数,它有2条yield语句,我们可以总结出4种状态:Initialstate:这个状态是gen的“状态机”的初始状态,不会做任何事物;状态1:初始状态的下一个状态,跳转到这个状态,执行console.log("state1");产生“state1”;状态2:这个状态会先接收到之前状态的数据,然后执行letstate1=data;console.log("state2");yield"state2";//注意,这里是最后一个yield,其中data是替换的在先前的状态下产生“state1”。状态三(终态):因为gen已经执行了最后一个yield表达式,所以状态三也是状态机的终态。这个状态也接受前一个状态的data数据,执行letstate2=data;console.log("end");同时,它还将迭代器返回的对象的done属性修改为true,如{value:undefined,done:true}。这意味着gen状态机已经执行到最终状态。将gen的Generator函数转化为状态机后,我们可以在脑海中想象出如下画面:接下来我们就根据这张图来分析状态是如何跳转的。首先是初始状态。当Generator函数执行时,状态机自动处于初始状态。该状态不执行任何语句。即执行语句:letg=gen();会有一个指向初始状态的箭头,如下图所示:然后是非初始状态之间的状态跳转。如果你想按照gen中定义的状态顺序跳转,那么你应该使用next()方法。比如我们第一次执行g.next()时,gen的状态机会从初始状态跳转到状态一。然后执行g.next(),状态1会跳转到状态2,发送数据undefined,因为next函数不传参,默认是undefined。我将在下一节中讨论如何在状态之间传递数据。当我们连续调用next方法时,gen会按照定义的流程进行状态跳转。而且即使到了最终状态,next也会返回对象,但是这个对象的值永远是{value:undefined,done:true}。听起来像是在finalstate之后又增加了一个新的state,所以next方法可以继续执行。但是我觉得为了符合状态机的设定,还是把第一个done状态称为final状态比较好。return方法不同于step-by-stepnext方法。return方法会打断原来的状态顺序,根据开发者的需要跳转到一个新的状态。这个状态有两个特点:它不是原状态序列中的任何一个状态;此状态返回的对象的done属性值为真。让我们继续上面的例子。如果从状态1跳转到状态2,使用的代码是g.return();而不是g.next(),那么状态图会是这样的:从图中可以看出,return的行为是new添加一个新的state2,插入到state1之后,然后从state1跳转到新状态2,同时输出{value:undefined,done:true}。同样,这里的undefined也是因为return方法没有传参。如果Generator函数中有try...finally语句,则新创建的return状态将插入到finally块中最后一行语句的状态之后。可以看看本节中阮一峰老师举的例子。throw方法我喜欢将throw方法视为next和return方法的组合。throw()方法与throw关键字非常相似,因为它会抛出错误。Generator函数会根据捕获语句是否定义进行状态跳转。一共有以下三种情况:没有try...catch;下一状态要执行的语句在try...catch中;throw()方法在try...catch中被调用。不使用try...catch,继续使用上一章的代码,假设从状态1到状态2使用了g.throw()。函数*gen(){console.log("state1");让state1=yield"state1";console.log("state2");让state2=yield"state2";console.log("end");}letg=gen();g.next();g.throw();首先代码console.log("state2");...不在try...catch块,它不是在try...catch块中调用g.throw()。那么最终的状态图应该是这样的:看起来调用了return方法,增加了一个新的状态,输出对象的done属性设置为true。但是有一点不同的是,这个对象不会被输出,而是会报错:Uncaughtundefined,因为程序出错被中断了。同样,原本要输出的字符串state2也不会输出。这里我觉得需要注意的一个问题是第二个状态的哪个语句抛出的错误?修改代码位置后发现throw()方法将yield"state1"替换为throwundefined,所以后面的letstate1...等语句不会执行。Nextstate在try...catch中修改上一章的示例代码:function*gen(){console.log("state1");试试{让state1=yield"state1";console.log("state2");}catch(e){console.log("抓住它");}letstate2=yield"state2";console.log("end");}letg=gen();g.next();g.throw();由于状态2要执行的代码被try...catch包裹,throw()抛出的错误被catch块捕获,所以程序直接转到catch块执行语句,打印“catchit”。这和JS的错误捕获机制是一致的,整体状态图不会变,只是状态第二节点下的执行语句会变。注意红圈内的说法。对比调用next方法时的state2,删掉letstate1=data;console.log("state2");在try块中抛出错误的位置之后,在Executeconsole.log("catchit");中加入catch块,如果有finally块,也会加入里面的语句。调用next方法后,还是会按照规定的流程跳转。这一次,throw方法对状态机的操作方式与next方法非常相似。但是因为他本质上是抛出一个错误,所以会对程序的代码执行顺序造成一定的影响。throw()方法是在一个try...catch中调用的,只要结合以上两种情况,记住三个规则:如果Genereator内部没有try...catch,就会被当作普通的抛出错误;nextstate在try...catch中,throw()方法抛出的错误会被捕获,相当于外部没有捕获到错误,与第二种情况一致。规则2中状态执行代码捕获到错误后报错,按照规则1处理,这里对规则3进行解释,看下面的例子:function*gen(){console.日志(“状态1”);试试{让state1=yield"state1";console.log("state2");}catch(e){err=a;//errorconsole.log("内部捕获");}letstate2=yield"state2";console.log("结束");}letg=gen();g.next();try{g.throw();}catch(e){console.log("Externalcapture");}那么原本符合规则2的代码就被捕获了被外层catch是因为捕获到throw()块捕获抛出的错误后没有声明标识符a。导致看起来像规则1的样子。除了状态跳转,状态间传值的next、throw、return方法还有一个作用,就是前后两个状态的传值。但是他们三个人的表现是不同的。next给state传值的表现还是比较满意的,看下面的代码:function*gen(){letvalue=yield"Hello";console.log(value);}letg=gen();g.next();g.next("goodbye");当我们要跳转到执行console.log(value);的第二种状态时,传递一个字符串"goodbye"给next方法,然后yield"hello"就会被替换成"goodbye",赋值给value变量并打印出来。你可以尽量不传值或者传其他值,这样应该能帮助你更深入地理解。throw方法一般是传值,应该传一个Error对象来规范。return方法传值有点特殊,修改上面代码:function*gen(){letvalue=yield"Hello";console.log(value);}让g=gen();g.next();g。return("你能看到我吗?");如果你没有忘记前面的知识,你应该知道,将next替换为return后,什么也不会打印。因为它会跳转到一个不会执行任何代码的状态。那么返回函数的参数函数体现在哪里呢?还记得每个方法调用都会返回一个对象吗?上面的代码输出{value:"Canyouseeme",done:true}。哈,我看见你了。关于最终状态,我一般喜欢把最后的yield或者return表达式当做最后状态。但是有时候你可以把最终状态想象成一个不断循环自己的状态,比如下面这样:这样理解的一个好处是可以解释为什么在done属性的值为true之后,再次调用next还是会返回一个对象{值:未定义,完成:真}。但是这样会多加一个状态,不方便画图(假装这个理由很好)。总之,怎么理解,全凭个人喜好。实际案例下面用状态机的思想讲两个实际案例。一个小问题之前回答过一个问题,我们举个例子来分析一下。题主不太理解以下代码的执行顺序:function*bar(){console.log('one');console.log('二');console.log('三');yieldconsole.log('test');console.log(`1.${yield}`);console.log(`2.${yield}`);return'result';}letbarObj=bar();barObj.next();barObj.next('a');barObj.next('b');让我们帮他分析一下。首先,我完成了这段代码。function*bar(){console.log('one');console.log('二');console.log('三');yieldconsole.log('test');console.log(`1.${yield}`);console.log(`2.${yield}`);return'result';}letbarObj=bar();barObj.next();barObj.next('a');barObj.next('b');barObj.next('c');barObj.next();然后分析bar的生成器声明的几个状态。一共有6个状态,状态图如下:根据状态图,题主提出的问题有两个:第一次nextshouldgotoyieldconsole.log('test')第二次aa传给程序似乎第一个问题没有实现。调用next方法后跳转到state1,yieldconsole.log('test')是在state1执行的,所以确实到了这行代码。然后,调用next("a")跳转到state2,这里没有值接收字符串"a",自然不会打印出来,造成程序没有执行的错觉。这个问题比较简单,状态图画一下就明白了。throw方法的一个特点第二个例子是我在看《ECMAScript 6 入门》的时候,阮一峰老师说:throw方法被捕获后,会执行下一个yield表达式。也就是说,next方法会被额外执行。然后举了个例子:vargen=function*gen(){try{yieldconsole.log('a');}catch(e){//...}yieldconsole.log('b');yieldconsole.log('c');}varg=gen();g.next()//ag.throw()//bg.next()//c我觉得这里很奇怪,因为根据我的idea,这个很明显,为什么要单独提?按照我Generator状态跳转一章说的,这属于下一个状态在try...catch的情况,因为yieldintry{/*state2*/yieldconsole.log('a');}左边是state2状态的代码。虽然没有写,但是我们默认给一个永远不会被调用的变量赋值。然后绘制状态图:我们只关心g.throw(),所以绘制部分状态图就够了。从图中可以看出,调用throw方法后,因为捕获到错误,所以正常跳转到state2,然后yieldconsole.log('b');必须执行。总结一下状态机的知识还是在大学的编译原理课上学的,有些概念忘记了。但是在看Generator的时候突然觉得用状态机来解释代码的冻结和执行是非常直观的。只要能画出相应的状态图,就可以知道每次调用next等方法时会执行什么样的代码。靠着状态机的思想,学习Generator的时候基本没有疑惑,所以决定整理出来分享一下。但是我有点不自信,因为我在网上搜索了很多次,除了阮一峰老师,没有人同时提到状态机和生成器这两个关键词。在写这篇文章时,我偶尔会想我是不是错了。不过既然写了这么多,从我自己的感受和文中解决两个例子的情况来看,分享出来让大家指出错误还是不错的。所以,如果有什么问题,请在评论中指出。非常感谢您阅读并祝您新年快乐!