亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專欄INFORMATION COLUMN

JS 用狀態(tài)機的思想看Generator之基本語法篇

moven_j / 1109人閱讀

摘要:首先是初態(tài),當(dāng)函數(shù)被執(zhí)行后,狀態(tài)機就自動處于初態(tài)了。這一次,方法對狀態(tài)機的操作與方法大體相同。實際案例下面利用狀態(tài)機的思想講講兩個實際案例??恐鵂顟B(tài)機的思想,我在學(xué)習(xí)時基本

前言

最近學(xué)習(xí)了阮一峰老師的《ECMAScript 6 入門》里的Generator相關(guān)知識,以及《你不知道的JS》中卷的異步編程部分。同時在SegmentFault問答區(qū)看到了一些前端朋友對Generator的語法和執(zhí)行過程有一些疑問,于是我想分享一下自己對Generator的理解,也許對前端社區(qū)會有所幫助。

Generator本質(zhì)

Generator的本質(zhì)是一個狀態(tài)機,yield關(guān)鍵字的作用是分割兩個狀態(tài),右邊的語句執(zhí)行在前一個狀態(tài),而左邊的語句是下一個狀態(tài)要執(zhí)行的。如果右邊為空則默認(rèn)為undefined,左邊為空默認(rèn)為一個賦值語句,被賦值的變量永遠(yuǎn)不會被調(diào)用。當(dāng)調(diào)用Generator函數(shù)獲取一個迭代器時,狀態(tài)機處于初態(tài)。迭代器調(diào)用next方法后,向下一個狀態(tài)跳轉(zhuǎn),然后執(zhí)行該狀態(tài)的代碼。當(dāng)遇到return或最后一個yield時,進(jìn)入終態(tài)。終態(tài)的標(biāo)識就是next方法返回對象的done屬性。

Generator狀態(tài)跳轉(zhuǎn)

Generator函數(shù)執(zhí)行后會生出一個迭代器,包含3個主要方法:next、throw和return。它們的本質(zhì)都是改變狀態(tài)機的狀態(tài),但throw和return屬于強制改變,next則是按照定義好的流程去改變。下面我來分別講講這三種方法。

next方法

先看下面這個例子:

function* gen(){
    console.log("state1");
    let state1 = yield "state1";
    console.log("state2");
    let state2 = yield "state2";
    console.log("end");
}

我們聲明了一個名為gen的Generator函數(shù),其中有2個yield語句,我們可以歸納出4個狀態(tài):

初態(tài):這個狀態(tài)是gen這個“狀態(tài)機”的初始狀態(tài),什么也不會做;

狀態(tài)一:初態(tài)的下一個狀態(tài),跳轉(zhuǎn)到這個狀態(tài)后執(zhí)行

console.log("state1");
yield "state1";

狀態(tài)二:這個狀態(tài)會先接收上一個狀態(tài)傳來的數(shù)據(jù)data,然后執(zhí)行

let state1 = data;
console.log("state2");
yield "state2";//注意,這里是最后一個yield

這里的data是對上一個狀態(tài)中yield "state1"的替換。

狀態(tài)三(終態(tài)):因為gen已經(jīng)執(zhí)行過最后一個yield表達(dá)式,所以狀態(tài)三也就是狀態(tài)機的終態(tài)。這個狀態(tài)也接受了上一個狀態(tài)傳來的數(shù)據(jù)data,執(zhí)行了

let state2 = data;
console.log("end");

同時,還將迭代器返回的對象done屬性修改為true,比如{value:undefined,done:true}。這代表gen這個狀態(tài)機已經(jīng)執(zhí)行到了終態(tài)。

將gen這個Generator函數(shù)轉(zhuǎn)換成狀態(tài)機以后,我們可以在腦中想象出下面這張圖:

接下來我們就根據(jù)這張圖分析下狀態(tài)間是如何跳轉(zhuǎn)的。

首先是初態(tài),當(dāng)Generator函數(shù)被執(zhí)行后,狀態(tài)機就自動處于初態(tài)了。這個狀態(tài)并不會執(zhí)行任何語句。
也就是執(zhí)行語句:

let g = gen();

會有一個箭頭指向初態(tài),如下圖:

然后是非初態(tài)間的狀態(tài)跳轉(zhuǎn)。如果你想要按照gen里定義好的狀態(tài)順序跳轉(zhuǎn),那你應(yīng)該使用next()方法。比如我們第一次執(zhí)行g.next(),gen這個狀態(tài)機會從初態(tài)跳轉(zhuǎn)到狀態(tài)一。然后再執(zhí)行g.next(),則狀態(tài)一會向狀態(tài)二跳轉(zhuǎn),并且發(fā)送數(shù)據(jù)undefined,這是因為next函數(shù)沒有傳參,默認(rèn)為undefined。關(guān)于狀態(tài)間如何傳遞數(shù)據(jù)我將在下一節(jié)講。

當(dāng)我們不斷調(diào)用next方法,gen會按照定義好的流程進(jìn)行狀態(tài)跳轉(zhuǎn)。而且即使是到了終態(tài),next也會返回對象,只是這個對象的值一直是{value:undefined,done:true}。聽上去像是在終態(tài)后面又新增了一個狀態(tài),所以next方法能夠不斷執(zhí)行。但是我覺得為了符合狀態(tài)機的設(shè)定,還是將第一個done為true的狀態(tài)叫做終態(tài)比較好。

return方法

與按部就班的next方法不同,return方法會打破原有的狀態(tài)序列,并根據(jù)開發(fā)者的需要跳轉(zhuǎn)到一個新的狀態(tài),而這個狀態(tài)有兩個特點:

不是原有狀態(tài)序列中的任何一個狀態(tài);

該狀態(tài)返回的對象的done屬性值為true。

我們繼續(xù)用上面的例子。如果從狀態(tài)一跳轉(zhuǎn)到狀態(tài)二,使用的代碼是g.return();而不是g.next(),那么狀態(tài)圖會變成下面這個樣子:

從圖中可以看出,return的行為就是新增一個新·狀態(tài)二插入在狀態(tài)一后面,然后從狀態(tài)一跳轉(zhuǎn)到新·狀態(tài)二,同時輸出{value:undefined,done:true}。同樣,這里的undefined也是因為return方法沒有傳參。

如果Generator函數(shù)里有一個try...finally語句,return新建的狀態(tài)會插入在執(zhí)行finally塊最后一行語句的狀態(tài)之后??梢钥纯催@一節(jié)阮一峰老師舉的例子。

throw方法

我喜歡將throw方法當(dāng)作next和return方法的結(jié)合。throw()方法與throw關(guān)鍵字很像,都是拋出一個錯誤。而Generator函數(shù)會根據(jù)是否定義捕獲語句來進(jìn)行狀態(tài)跳轉(zhuǎn)。一共有下面3種情況:

沒有try...catch;

下一個狀態(tài)要執(zhí)行的語句在try...catch中;

throw()方法在一個try...catch中被調(diào)用。

沒有try...catch

繼續(xù)使用上一章的代碼,假設(shè)從狀態(tài)一到狀態(tài)二使用的是g.throw()。

function* gen(){
    console.log("state1");
    let state1 = yield "state1";
    console.log("state2");
    let state2 = yield "state2";
    console.log("end");
}
let g = gen();
g.next();
g.throw();

首先,狀態(tài)二的代碼console.log("state2");...并不在try...catch塊中,而且也不是在try...catch塊中調(diào)用g.throw()。那么最后的狀態(tài)圖應(yīng)該是下面這樣:

看上去就像是調(diào)用了return方法,新增一個狀態(tài),同時將輸出的對象done屬性設(shè)置為true。但是有一點不同的是這個對象并不會輸出,而是報錯:Uncaught undefined,因為程序因錯誤而中斷。同樣,原本要輸出的字符串state2也不會輸出。

這里我認(rèn)為需要重視的一個問題是錯誤是在狀態(tài)二中的哪一條語句拋出的?修改了代碼位置后,我發(fā)現(xiàn)throw()方法是將yield "state1"替換成throw undefined,所以之后的let state1...等語句都不會執(zhí)行。

下一個狀態(tài)在try...catch中

修改上一章的示例代碼:

function* gen(){
    console.log("state1");
    try{
        let state1 = yield "state1";
        console.log("state2");
    }catch(e){
        console.log("catch it");
    }
    let state2 = yield "state2";
    console.log("end");
}
let g = gen();
g.next();
g.throw();

由于狀態(tài)二要執(zhí)行的代碼被try...catch包裹,所以throw()拋出的錯誤被catch塊捕獲,從而程序直接轉(zhuǎn)入catch塊執(zhí)行語句,打印“catch it”。這與JS的錯誤捕獲機制一致,狀態(tài)圖總體并不會變化,只是狀態(tài)二節(jié)點下的執(zhí)行語句有變化。

注意紅色圈內(nèi)的語句,相比較與調(diào)用next方法時的狀態(tài)二,刪除了try塊中錯誤拋出位置后的let state1 = data;console.log("state2");,添加了catch塊中要執(zhí)行的console.log("catch it");,如果有finally塊也會把里面的語句添加進(jìn)去。之后再調(diào)用next方法,仍然會按照規(guī)定好的流程進(jìn)行跳轉(zhuǎn)。

這一次,throw方法對狀態(tài)機的操作與next方法大體相同。但因為他本質(zhì)上是拋出錯誤,所以會對程序的代碼執(zhí)行順序有一定的影響。

throw()方法在一個try...catch中被調(diào)用

只要結(jié)合上面2種情況,記住3個規(guī)則就行:

Genereator內(nèi)部沒有try...catch則當(dāng)作正常拋出錯誤處理;

下一個狀態(tài)在try...catch中時,throw()方法拋出的錯誤會被捕獲,那相當(dāng)于外部沒有捕獲錯誤,與第二種情況一致。

規(guī)則2中錯誤捕獲后的狀態(tài)執(zhí)行代碼報錯,按規(guī)則1處理。

這里,針對規(guī)則3做一個講解。

看下面這個例子:

function* gen(){
    console.log("state1");
    try{
        let state1 = yield "state1";
        console.log("state2");
    }catch(e){
        err = a;//錯誤
        console.log("內(nèi)部捕獲");
    }
    let state2 = yield "state2";
    console.log("end");
}
let g = gen();
g.next();
try{
    g.throw();
}catch(e){
    console.log("外部捕獲");
}

那么原本符合規(guī)則2的代碼在捕獲throw()拋出的錯誤后又因為沒有聲明標(biāo)識符a報錯,從而被外層catch塊捕獲。導(dǎo)致看上去就像規(guī)則1一樣。

狀態(tài)間傳值

next、throw和return方法除了狀態(tài)跳轉(zhuǎn)外,還有一個功能就是為前后兩個狀態(tài)傳值。但是它們3個的表現(xiàn)又各不相同。

next給狀態(tài)傳值的表現(xiàn)中規(guī)中矩,看看下面的代碼:

function* gen(){
    let value = yield "你好";
    console.log(value);
}
let g = gen();
g.next();
g.next("再見");

當(dāng)我們想要跳轉(zhuǎn)到執(zhí)行console.log(value);的狀態(tài)二時,給next方法傳一個字符串“再見”,然后yield "你好"會被替換成"再見",賦值給value變量打印出來。你可以試試不傳值或者傳其他值,應(yīng)該能幫助你理解更深刻。

throw方法一般都會傳值,而且為了規(guī)范應(yīng)該傳一個Error對象。

return方法傳值有點特殊,修改上面的代碼:

function* gen(){
    let value = yield "你好";
    console.log(value);
}
let g = gen();
g.next();
g.return("看得見我嗎?");

如果你前面的知識沒忘的話,你應(yīng)該知道,用return替換next后,什么也不會打印。因為跳轉(zhuǎn)到了一個什么代碼也不會執(zhí)行狀態(tài)。那么return函數(shù)的參數(shù)作用體現(xiàn)在哪呢?還記得每一個方法調(diào)用后都會返回一個對象嗎?上面的代碼輸出了{value:"看得見我嗎",done:true}。哈,我看見你了。

關(guān)于終態(tài)

一般我喜歡把最后一個yield或是return表達(dá)式當(dāng)作最后一個狀態(tài)。但是有時候可以把終態(tài)想象成一個不斷循環(huán)自身的狀態(tài),比如下面這樣:

這樣理解有一個好處是可以解釋為什么done屬性值為true后,再次調(diào)用next仍會返回一個對象{value:undefined,done:true}。但是這樣會多一個狀態(tài),畫圖不方便(假裝這個理由很充分)。
總之,如何理解全看個人喜歡。

實際案例

下面利用狀態(tài)機的思想講講兩個實際案例。

一個小問題

我之前回答過一個問題,把它當(dāng)作實例來分析一下吧

題主不太理解下面代碼的執(zhí)行順序:

function* bar() {
  console.log("one");
  console.log("two");
  console.log("three");
  yield console.log("test");
  console.log(`1. ${yield}`);
  console.log(`2. ${yield}`);
  return "result";
}
let barObj = bar();
barObj.next();
barObj.next("a");
barObj.next("b");

讓我們來幫他分析分析吧。

首先,我補全了這段代碼。

function* bar() {
  console.log("one");
  console.log("two");
  console.log("three");
  yield console.log("test");
  console.log(`1. ${yield}`);
  console.log(`2. ${yield}`);
  return "result";
}
let barObj = bar();
barObj.next();
barObj.next("a");
barObj.next("b");
barObj.next("c");
barObj.next();

然后,分析bar這個Genereator聲明了幾個狀態(tài)。一共有6個狀態(tài),狀態(tài)圖如下:

根據(jù)狀態(tài)圖,題主提出的兩個問題:

第一次 next 的時候應(yīng)該走到了 yield console.log("test")

第二次傳了一個 a 這個時候程序似乎沒有執(zhí)行

第一個問題,調(diào)用next方法后,跳轉(zhuǎn)到state1,而yield console.log("test")是在state1里執(zhí)行的,所以確實走到了這行代碼。

然后,調(diào)用next("a"),跳轉(zhuǎn)到state2,這里并沒有值接收字符串"a",所以自然沒有打印出來,造成程序沒有執(zhí)行的假象。

這個問題比較簡單,狀態(tài)圖一畫就能理解了。

throw方法的一個特性

第二個實例是我在看《ECMAScript 6 入門》時,阮一峰老師說:

throw方法被捕獲以后,會附帶執(zhí)行下一條yield表達(dá)式。也就是說,會附帶執(zhí)行一次next方法。

然后舉了一個例子:

var gen = function* gen(){
  try {
    yield console.log("a");
  } catch (e) {
    // ...
  }
  yield console.log("b");
  yield console.log("c");
}

var g = gen();
g.next() // a
g.throw() // b
g.next() // c

這里我覺得很奇怪,因為按照我的想法,這是顯然的呀,為什么要多帶帶說呢?按照我在Generator狀態(tài)跳轉(zhuǎn)那一章說的,這屬于下一個狀態(tài)在try...catch中的情況,因為

try{
    /*state2*/yield console.log("a");
}

中yield的左側(cè)是state2狀態(tài)的代碼,雖然沒有寫,但是我們默認(rèn)為向一個永遠(yuǎn)不會被調(diào)用的變量進(jìn)行賦值。
接著是畫狀態(tài)圖:

我們只關(guān)心g.throw(),所以畫部分狀態(tài)圖就夠了。從圖中可以看出,throw方法被調(diào)用后,因為錯誤被捕獲,所以正常跳轉(zhuǎn)到了state2,然后必然會執(zhí)行yield console.log("b");。

總結(jié)

狀態(tài)機的知識還是在大學(xué)的編譯原理課學(xué)習(xí)的,有些概念已經(jīng)忘了。不過在看Generator時,我突然覺得用狀態(tài)機來解釋代碼的凍結(jié)和執(zhí)行非常直觀。只要能夠畫出相應(yīng)的狀態(tài)圖就可以知道每一次調(diào)用next等方法會執(zhí)行什么樣的代碼。靠著狀態(tài)機的思想,我在學(xué)習(xí)Generator時基本沒有疑惑,所以決定整理并分享出來。
但是我有點不自信,因為網(wǎng)上搜索了很多次,除了阮一峰老師,并沒有人同時提到狀態(tài)機和Generator兩個關(guān)鍵字。我在寫這篇文章的時候也偶爾懷疑是不是我錯了。不過既然已經(jīng)寫了這么多,而且從我自身感覺以及解決了文中兩個例子的情況來看,分享出來讓大家指指錯也是不錯的。 所以,如果有什么問題希望能夠在評論中指出。非常感謝你的閱讀,祝你新年快樂!

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/107426.html

相關(guān)文章

  • 探索Javascript 異步編程

    摘要:因為瀏覽器環(huán)境里是單線程的,所以異步編程在前端領(lǐng)域尤為重要。除此之外,它還有兩個特性,使它可以作為異步編程的完整解決方案函數(shù)體內(nèi)外的數(shù)據(jù)交換和錯誤處理機制。 showImg(https://segmentfault.com/img/bVz9Cy); 在我們?nèi)粘>幋a中,需要異步的場景很多,比如讀取文件內(nèi)容、獲取遠(yuǎn)程數(shù)據(jù)、發(fā)送數(shù)據(jù)到服務(wù)端等。因為瀏覽器環(huán)境里Javascript是單線程的,...

    Salamander 評論0 收藏0
  • JS筆記

    摘要:從最開始的到封裝后的都在試圖解決異步編程過程中的問題。為了讓編程更美好,我們就需要引入來降低異步編程的復(fù)雜性。異步編程入門的全稱是前端經(jīng)典面試題從輸入到頁面加載發(fā)生了什么這是一篇開發(fā)的科普類文章,涉及到優(yōu)化等多個方面。 TypeScript 入門教程 從 JavaScript 程序員的角度總結(jié)思考,循序漸進(jìn)的理解 TypeScript。 網(wǎng)絡(luò)基礎(chǔ)知識之 HTTP 協(xié)議 詳細(xì)介紹 HTT...

    rottengeek 評論0 收藏0
  • ES6 Generator實現(xiàn)協(xié)同程序

    摘要:關(guān)鍵字表示代碼在該處將會被阻塞式暫停阻塞的僅僅是函數(shù)代碼本身,而不是整個程序,但是這并沒有引起函數(shù)內(nèi)部自頂向下代碼的絲毫改變。通過實現(xiàn)模式在通過實現(xiàn)理論的過程中已經(jīng)有一些有趣的探索了。 至此本系列的四篇文章翻譯完結(jié),查看完整系列請移步blogs 由于個人能力知識有限,翻譯過程中難免有紕漏和錯誤,望不吝指正issue ES6 Generators: 完整系列 The Basics...

    MudOnTire 評論0 收藏0
  • ES6-7

    摘要:的翻譯文檔由的維護(hù)很多人說,阮老師已經(jīng)有一本關(guān)于的書了入門,覺得看看這本書就足夠了。前端的異步解決方案之和異步編程模式在前端開發(fā)過程中,顯得越來越重要。為了讓編程更美好,我們就需要引入來降低異步編程的復(fù)雜性。 JavaScript Promise 迷你書(中文版) 超詳細(xì)介紹promise的gitbook,看完再不會promise...... 本書的目的是以目前還在制定中的ECMASc...

    mudiyouyou 評論0 收藏0

發(fā)表評論

0條評論

moven_j

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<