摘要:首先是初態,當函數被執行后,狀態機就自動處于初態了。這一次,方法對狀態機的操作與方法大體相同。實際案例下面利用狀態機的思想講講兩個實際案例。靠著狀態機的思想,我在學習時基本
前言
最近學習了阮一峰老師的《ECMAScript 6 入門》里的Generator相關知識,以及《你不知道的JS》中卷的異步編程部分。同時在SegmentFault問答區看到了一些前端朋友對Generator的語法和執行過程有一些疑問,于是我想分享一下自己對Generator的理解,也許對前端社區會有所幫助。
Generator本質Generator的本質是一個狀態機,yield關鍵字的作用是分割兩個狀態,右邊的語句執行在前一個狀態,而左邊的語句是下一個狀態要執行的。如果右邊為空則默認為undefined,左邊為空默認為一個賦值語句,被賦值的變量永遠不會被調用。當調用Generator函數獲取一個迭代器時,狀態機處于初態。迭代器調用next方法后,向下一個狀態跳轉,然后執行該狀態的代碼。當遇到return或最后一個yield時,進入終態。終態的標識就是next方法返回對象的done屬性。
Generator狀態跳轉Generator函數執行后會生出一個迭代器,包含3個主要方法:next、throw和return。它們的本質都是改變狀態機的狀態,但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函數,其中有2個yield語句,我們可以歸納出4個狀態:
初態:這個狀態是gen這個“狀態機”的初始狀態,什么也不會做;
狀態一:初態的下一個狀態,跳轉到這個狀態后執行
console.log("state1"); yield "state1";
狀態二:這個狀態會先接收上一個狀態傳來的數據data,然后執行
let state1 = data; console.log("state2"); yield "state2";//注意,這里是最后一個yield
這里的data是對上一個狀態中yield "state1"的替換。
狀態三(終態):因為gen已經執行過最后一個yield表達式,所以狀態三也就是狀態機的終態。這個狀態也接受了上一個狀態傳來的數據data,執行了
let state2 = data; console.log("end");
同時,還將迭代器返回的對象done屬性修改為true,比如{value:undefined,done:true}。這代表gen這個狀態機已經執行到了終態。
將gen這個Generator函數轉換成狀態機以后,我們可以在腦中想象出下面這張圖:
接下來我們就根據這張圖分析下狀態間是如何跳轉的。
首先是初態,當Generator函數被執行后,狀態機就自動處于初態了。這個狀態并不會執行任何語句。
也就是執行語句:
let g = gen();
會有一個箭頭指向初態,如下圖:
然后是非初態間的狀態跳轉。如果你想要按照gen里定義好的狀態順序跳轉,那你應該使用next()方法。比如我們第一次執行g.next(),gen這個狀態機會從初態跳轉到狀態一。然后再執行g.next(),則狀態一會向狀態二跳轉,并且發送數據undefined,這是因為next函數沒有傳參,默認為undefined。關于狀態間如何傳遞數據我將在下一節講。
當我們不斷調用next方法,gen會按照定義好的流程進行狀態跳轉。而且即使是到了終態,next也會返回對象,只是這個對象的值一直是{value:undefined,done:true}。聽上去像是在終態后面又新增了一個狀態,所以next方法能夠不斷執行。但是我覺得為了符合狀態機的設定,還是將第一個done為true的狀態叫做終態比較好。
return方法與按部就班的next方法不同,return方法會打破原有的狀態序列,并根據開發者的需要跳轉到一個新的狀態,而這個狀態有兩個特點:
不是原有狀態序列中的任何一個狀態;
該狀態返回的對象的done屬性值為true。
我們繼續用上面的例子。如果從狀態一跳轉到狀態二,使用的代碼是g.return();而不是g.next(),那么狀態圖會變成下面這個樣子:
從圖中可以看出,return的行為就是新增一個新·狀態二插入在狀態一后面,然后從狀態一跳轉到新·狀態二,同時輸出{value:undefined,done:true}。同樣,這里的undefined也是因為return方法沒有傳參。
如果Generator函數里有一個try...finally語句,return新建的狀態會插入在執行finally塊最后一行語句的狀態之后。可以看看這一節阮一峰老師舉的例子。
throw方法我喜歡將throw方法當作next和return方法的結合。throw()方法與throw關鍵字很像,都是拋出一個錯誤。而Generator函數會根據是否定義捕獲語句來進行狀態跳轉。一共有下面3種情況:
沒有try...catch;
下一個狀態要執行的語句在try...catch中;
throw()方法在一個try...catch中被調用。
沒有try...catch繼續使用上一章的代碼,假設從狀態一到狀態二使用的是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();
首先,狀態二的代碼console.log("state2");...并不在try...catch塊中,而且也不是在try...catch塊中調用g.throw()。那么最后的狀態圖應該是下面這樣:
看上去就像是調用了return方法,新增一個狀態,同時將輸出的對象done屬性設置為true。但是有一點不同的是這個對象并不會輸出,而是報錯:Uncaught undefined,因為程序因錯誤而中斷。同樣,原本要輸出的字符串state2也不會輸出。
這里我認為需要重視的一個問題是錯誤是在狀態二中的哪一條語句拋出的?修改了代碼位置后,我發現throw()方法是將yield "state1"替換成throw undefined,所以之后的let state1...等語句都不會執行。
下一個狀態在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();
由于狀態二要執行的代碼被try...catch包裹,所以throw()拋出的錯誤被catch塊捕獲,從而程序直接轉入catch塊執行語句,打印“catch it”。這與JS的錯誤捕獲機制一致,狀態圖總體并不會變化,只是狀態二節點下的執行語句有變化。
注意紅色圈內的語句,相比較與調用next方法時的狀態二,刪除了try塊中錯誤拋出位置后的let state1 = data;console.log("state2");,添加了catch塊中要執行的console.log("catch it");,如果有finally塊也會把里面的語句添加進去。之后再調用next方法,仍然會按照規定好的流程進行跳轉。
這一次,throw方法對狀態機的操作與next方法大體相同。但因為他本質上是拋出錯誤,所以會對程序的代碼執行順序有一定的影響。
throw()方法在一個try...catch中被調用只要結合上面2種情況,記住3個規則就行:
Genereator內部沒有try...catch則當作正常拋出錯誤處理;
下一個狀態在try...catch中時,throw()方法拋出的錯誤會被捕獲,那相當于外部沒有捕獲錯誤,與第二種情況一致。
規則2中錯誤捕獲后的狀態執行代碼報錯,按規則1處理。
這里,針對規則3做一個講解。
看下面這個例子:
function* gen(){ console.log("state1"); try{ let state1 = yield "state1"; console.log("state2"); }catch(e){ err = a;//錯誤 console.log("內部捕獲"); } let state2 = yield "state2"; console.log("end"); } let g = gen(); g.next(); try{ g.throw(); }catch(e){ console.log("外部捕獲"); }
那么原本符合規則2的代碼在捕獲throw()拋出的錯誤后又因為沒有聲明標識符a報錯,從而被外層catch塊捕獲。導致看上去就像規則1一樣。
狀態間傳值next、throw和return方法除了狀態跳轉外,還有一個功能就是為前后兩個狀態傳值。但是它們3個的表現又各不相同。
next給狀態傳值的表現中規中矩,看看下面的代碼:
function* gen(){ let value = yield "你好"; console.log(value); } let g = gen(); g.next(); g.next("再見");
當我們想要跳轉到執行console.log(value);的狀態二時,給next方法傳一個字符串“再見”,然后yield "你好"會被替換成"再見",賦值給value變量打印出來。你可以試試不傳值或者傳其他值,應該能幫助你理解更深刻。
throw方法一般都會傳值,而且為了規范應該傳一個Error對象。
return方法傳值有點特殊,修改上面的代碼:
function* gen(){ let value = yield "你好"; console.log(value); } let g = gen(); g.next(); g.return("看得見我嗎?");
如果你前面的知識沒忘的話,你應該知道,用return替換next后,什么也不會打印。因為跳轉到了一個什么代碼也不會執行狀態。那么return函數的參數作用體現在哪呢?還記得每一個方法調用后都會返回一個對象嗎?上面的代碼輸出了{value:"看得見我嗎",done:true}。哈,我看見你了。
關于終態一般我喜歡把最后一個yield或是return表達式當作最后一個狀態。但是有時候可以把終態想象成一個不斷循環自身的狀態,比如下面這樣:
這樣理解有一個好處是可以解釋為什么done屬性值為true后,再次調用next仍會返回一個對象{value:undefined,done:true}。但是這樣會多一個狀態,畫圖不方便(假裝這個理由很充分)。
總之,如何理解全看個人喜歡。
下面利用狀態機的思想講講兩個實際案例。
一個小問題我之前回答過一個問題,把它當作實例來分析一下吧
題主不太理解下面代碼的執行順序:
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聲明了幾個狀態。一共有6個狀態,狀態圖如下:
根據狀態圖,題主提出的兩個問題:
第一次 next 的時候應該走到了 yield console.log("test")
第二次傳了一個 a 這個時候程序似乎沒有執行
第一個問題,調用next方法后,跳轉到state1,而yield console.log("test")是在state1里執行的,所以確實走到了這行代碼。
然后,調用next("a"),跳轉到state2,這里并沒有值接收字符串"a",所以自然沒有打印出來,造成程序沒有執行的假象。
這個問題比較簡單,狀態圖一畫就能理解了。
throw方法的一個特性第二個實例是我在看《ECMAScript 6 入門》時,阮一峰老師說:
throw方法被捕獲以后,會附帶執行下一條yield表達式。也就是說,會附帶執行一次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狀態跳轉那一章說的,這屬于下一個狀態在try...catch中的情況,因為
try{ /*state2*/yield console.log("a"); }
中yield的左側是state2狀態的代碼,雖然沒有寫,但是我們默認為向一個永遠不會被調用的變量進行賦值。
接著是畫狀態圖:
我們只關心g.throw(),所以畫部分狀態圖就夠了。從圖中可以看出,throw方法被調用后,因為錯誤被捕獲,所以正常跳轉到了state2,然后必然會執行yield console.log("b");。
狀態機的知識還是在大學的編譯原理課學習的,有些概念已經忘了。不過在看Generator時,我突然覺得用狀態機來解釋代碼的凍結和執行非常直觀。只要能夠畫出相應的狀態圖就可以知道每一次調用next等方法會執行什么樣的代碼。靠著狀態機的思想,我在學習Generator時基本沒有疑惑,所以決定整理并分享出來。
但是我有點不自信,因為網上搜索了很多次,除了阮一峰老師,并沒有人同時提到狀態機和Generator兩個關鍵字。我在寫這篇文章的時候也偶爾懷疑是不是我錯了。不過既然已經寫了這么多,而且從我自身感覺以及解決了文中兩個例子的情況來看,分享出來讓大家指指錯也是不錯的。 所以,如果有什么問題希望能夠在評論中指出。非常感謝你的閱讀,祝你新年快樂!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/107426.html
摘要:因為瀏覽器環境里是單線程的,所以異步編程在前端領域尤為重要。除此之外,它還有兩個特性,使它可以作為異步編程的完整解決方案函數體內外的數據交換和錯誤處理機制。 showImg(https://segmentfault.com/img/bVz9Cy); 在我們日常編碼中,需要異步的場景很多,比如讀取文件內容、獲取遠程數據、發送數據到服務端等。因為瀏覽器環境里Javascript是單線程的,...
摘要:從最開始的到封裝后的都在試圖解決異步編程過程中的問題。為了讓編程更美好,我們就需要引入來降低異步編程的復雜性。異步編程入門的全稱是前端經典面試題從輸入到頁面加載發生了什么這是一篇開發的科普類文章,涉及到優化等多個方面。 TypeScript 入門教程 從 JavaScript 程序員的角度總結思考,循序漸進的理解 TypeScript。 網絡基礎知識之 HTTP 協議 詳細介紹 HTT...
摘要:關鍵字表示代碼在該處將會被阻塞式暫停阻塞的僅僅是函數代碼本身,而不是整個程序,但是這并沒有引起函數內部自頂向下代碼的絲毫改變。通過實現模式在通過實現理論的過程中已經有一些有趣的探索了。 至此本系列的四篇文章翻譯完結,查看完整系列請移步blogs 由于個人能力知識有限,翻譯過程中難免有紕漏和錯誤,望不吝指正issue ES6 Generators: 完整系列 The Basics...
摘要:的翻譯文檔由的維護很多人說,阮老師已經有一本關于的書了入門,覺得看看這本書就足夠了。前端的異步解決方案之和異步編程模式在前端開發過程中,顯得越來越重要。為了讓編程更美好,我們就需要引入來降低異步編程的復雜性。 JavaScript Promise 迷你書(中文版) 超詳細介紹promise的gitbook,看完再不會promise...... 本書的目的是以目前還在制定中的ECMASc...
閱讀 1269·2021-09-23 11:51
閱讀 1385·2021-09-04 16:45
閱讀 629·2019-08-30 15:54
閱讀 2080·2019-08-30 15:52
閱讀 1599·2019-08-30 11:17
閱讀 3104·2019-08-29 13:59
閱讀 2014·2019-08-28 18:09
閱讀 385·2019-08-26 12:15