ES6 Generators基本概念

来源:http://www.cnblogs.com/jaxu/archive/2017/02/07/6372809.html
-Advertisement-
Play Games

ES6 Generators系列: 在JavaScript ES6提供的諸多令人興奮的新特性中,有一個新函數類型,叫generator。名字聽起來很怪(我們姑且將它稱之為生成器函數),而且行為更加讓人覺得怪異。本文旨在解釋generator函數的一些基本知識,用來說明它是如何工作的,並幫助你瞭解為什 ...


  ES6 Generators系列:

  1. ES6 Generators基本概念
  2. 深入研究ES6 Generators
  3. ES6 Generators的非同步應用
  4. ES6 Generators併發

  在JavaScript ES6提供的諸多令人興奮的新特性中,有一個新函數類型,叫generator。名字聽起來很怪(我們姑且將它稱之為生成器函數),而且行為更加讓人覺得怪異。本文旨在解釋generator函數的一些基本知識,用來說明它是如何工作的,並幫助你瞭解為什麼它會讓未來的JS變得如此強大。

 

運行-完成(Run-To-Completion)

  首先我們要討論的是generator函數和普通函數在運行方式上有什麼區別。

  不論你是否已經意識到了,對於函數而言,你總是會假定一個原則:一旦函數開始運行,它就會在其它JS代碼運行之前運行到結束。這句話怎麼理解呢?看下麵的代碼:

setTimeout(function(){
    console.log("Hello World");
},1);

function foo() {
    // NOTE: don't ever do crazy long-running loops like this
    for (var i=0; i<=1E10; i++) {
        console.log(i);
    }
}

foo();
// 0..1E10
// "Hello World"

  這裡的for迴圈需要一個比較長的時間來執行完,顯然超過1毫秒。在foo()函數運行過程中,上面的setTimeout函數不會被運行直到foo()函數運行結束。

  那如果事情不是這樣的會怎麼樣?如果foo()函數的運行會被setTimeout打斷呢?是不是我們的程式將會變得不穩定?

  在多線程運行的程式中,這的確會給你帶來噩夢,好在JavaScript是單線程運行的(同一時間只有一條命令或函數會被運行),因此這一點你不必擔心。

  註意,Web開發允許JS程式的一部分在一個獨立的線程里運行,該線程可以與JS主線程並行運行。但這並不意味著我們可以在JS程式中引入多線程操作,因為在多線程操作中兩個獨立的線程之間是可以通過非同步事件相互通信的,它們彼此之間通過事件輪詢機制(event-loop)一次一個地來運行。

 

運行-停止-運行(Run-Stop-Run)

  ES6的generator函數允許在運行的過程中暫停一次或多次,隨後再恢復運行。暫停的過程中允許其它的代碼執行。

  如果你曾經讀過有關併發或者線程編程方面的文章,你也許見到過"cooperative"(協作)一詞,它說明瞭一個進程(這裡可以將它理解為一個function)本身可以選擇何時被中斷以便與其它代碼進行協作。這個概念與"preemptive"(搶占式。進程調度的一種方式。當前進程在運行過程中,如果有重要或緊迫的進程到達(其狀態必須為就緒),則該進程將被迫放棄處理機,系統將處理機立刻分配給新到達的進程。)正好相反,它表明瞭一個進程或function可以被其自身的意願打斷。

  在ES6中,generator函數使用的都是cooperative類型的併發方式。在generator函數體內,通過使用新的yield關鍵字從內部將函數的運行打斷。除了generator函數內部的yield關鍵字,你不可能從任何地方(包括函數外部)中斷函數的運行。

  不過,一旦generator函數被中斷,它不可能自行恢復運行,除非通過外部的控制來重新啟動這個generator函數。稍後我會介紹如何實現這一點。

  基本上,按照需要,一個generator函數在運行中可以被停止和重新啟動多次。事實上,你完全可以指定一個無限迴圈的generator函數(就像while(true){...}語句一樣),它永遠也不會被執行完。不過在一個正常的JS程式中,我們通常不會這樣做,除非代碼寫錯了。Generator函數足夠理性,有時候它恰恰就是你想要的!

  而更重要的是,這種停止和啟動不僅僅控制著generator函數的執行,它還允許信息的雙向傳遞。普通函數在開始的時候獲取參數,在結束的時候return一個值,而generator函數可以在每次yield的時候返回值,並且在下一次重新啟動的時候再傳入值。

 

語法

  是時候介紹一下generator函數的語法了:

function *foo() {
    // ..
}

  註意這裡的*了嗎?這是一個新引入的運算符,對於學習C語言系的同學而言,可能會想到函數指針。不過這裡千萬不要把它和指針的概念混淆了,*運算符在這裡只是用來標識generator函數的類型。

  你可能在其它的文章或文檔中看到這種寫法function* foo(){},而本文中我們使用這種寫法function *foo(){}(區別僅僅是*的位置)。這兩種寫法都是正確的,不過我們推薦使用後者。

  我們來看看generator函數的內容。Generator函數在大多數方面就是普通的JS函數,因此我們需要學習的新語法不會很多。

  在generator函數體內部主要是yield關鍵字的應用,前面我們已經提到過它。註意這裡的yield ___被稱之為yield表達式而不是語句,這是因為當我們重新啟動generator函數時,我們會傳入一個值,而不管這個值是什麼,都會作為yield ___表達式計算的結果。

  一個例子:

function *foo() {
    var x = 1 + (yield "foo");
    console.log(x);
}

  這裡的yield "foo"表達式會在generator函數暫停時返回字元串"foo",當下一次generator函數重新啟動時,不管傳入的值是什麼,都會作為yield表達式計算的結果。這裡會將表達式1 + 傳入值的結果賦值給變數x

  從這個意義上來說,generator函數具有雙向通信的功能。Generator函數暫停的時候返回了字元串"foo",稍後(可能是立即,也可能是從現在開始一段很長的時間)重新啟動的時候它會請求一個新值並將最終計算的結果返回。這裡的yield關鍵字起到了請求新值的作用。

  在任何表達式中,你可以只用yield關鍵字而不帶其它內容,此時yield返回的值是undefined。看下麵的例子:

// 註意,這裡的函數foo(..)不是一個generator函數!!
function foo(x) {
    console.log("x: " + x);
}

function *bar() {
    yield; // 暫停執行,返回值是undefined
    foo( yield ); // 暫停執行,稍後將獲取到的值作為函數foo(..)的參數傳入
}

 

Generator遍歷器

  “Generator遍歷器”!乍一看,好像很難懂!

  遍歷器是一種特殊的行為,實際上是一種設計模式,我們通過調用next()方法來遍歷一組有序的值。想象一下,例如使用遍歷器對數組[1,2,3,4,5]進行遍歷。第一次調用next()方法返回1,第二次調用next()方法返回2,以此類推。當數組中的所有值都返回後,調用next()方法將返回nullfalse或其它可能的值用來表示數組中的所有元素都已遍歷完畢。

  我們唯一可以從外部控制generator函數的方式就是構造和通過遍歷器進行遍歷。這聽起來好像有點複雜,考慮下麵這個簡單的例子:

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

  為了遍歷generator函數*foo(),首先我們需要構造一個遍歷器。怎麼做?很簡單!

var it = foo();

  事實上,通過普通的方式調用一個generator函數並不會真正地執行它。

  這有點讓人難以理解。你可能在想,為什麼不是var it = new foo(). 背後的原理已經超出了我們的範圍,這裡我們不展開討論。

  然後,我們通過下麵的方法對generator函數進行遍歷:

var message = it.next();

  這會執行yield 1表達式並返回值1,但不僅限於此。

console.log(message); // { value:1, done:false }

  事實上每次調用next()方法都會返回一個object對象,其中的value屬性就是yield表達式返回的值,而屬性done是一個boolean類型,用來表示對generator函數的遍歷是否已經結束。

  繼續看剩餘的幾個遍歷:

console.log( it.next() ); // { value:2, done:false }
console.log( it.next() ); // { value:3, done:false }
console.log( it.next() ); // { value:4, done:false }
console.log( it.next() ); // { value:5, done:false }

  有趣的是,當value的值是5done仍然是false。這是因為從技術上來說,generator函數還沒有執行完,我們必須再調用一次next()方法,如果此時傳入一個值(如果未傳入值,則預設為undefined),它會被設置為yield 5表達式計算的結果,然後generator函數才算執行完畢。

  因此:

console.log( it.next() ); // { value:undefined, done:true }

  所以,最終的結果是我們完成了generator函數的調用,但是最後一次的遍歷並沒有返回任何值,這是因為所有的yield表達式都已經被執行完了。

  你或許在想,我們可以在generator函數中使用return語句嗎?如果可以的話,那value屬性的值會被返回嗎?

答案是肯定的:

function *foo() {
    yield 1;
    return 2;
}

var it = foo();

console.log( it.next() ); // { value:1, done:false }
console.log( it.next() ); // { value:2, done:true }

但是:

  依賴generator函數中return語句返回的值並不值得提倡,因為當使用for..of迴圈(下麵會介紹)來遍歷generator函數時,最後的return語句可能會導致異常。

  我們來完整地看一下在遍歷generator函數時信息是如何被傳入和傳出的:

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

var it = foo( 5 );

// 註意這裡在調用next()方法時沒有傳入任何值
console.log( it.next() );       // { value:6, done:false }
console.log( it.next( 12 ) );   // { value:8, done:false }
console.log( it.next( 13 ) );   // { value:42, done:true }

  你可以看到我們在構造generator函數遍歷器的時候仍然可以傳遞參數,這和普通的函數調用一樣,通過語句foo(5),我們將參數x的值設置為5。

  第一次調用next()方法時,沒有傳入任何值。為什麼呢?因為此時沒有yield表達式來接收我們傳入的值。

  如果在第一次調用next()方法時傳入一個值,也不會有任何影響,該值會被拋棄掉。按照ES6標準的規定,此時generator函數會直接忽略掉該值(註意:在撰寫本文時,Chrome和FireFox瀏覽器都能很好地符合該規定,但其它瀏覽器可能並不完全符合,而且可能會拋出異常)。

  表達式yield(x + 1)的返回值是6,然後第二個next(12)12作為參數傳入,用來代替表達式yield(x + 1),因此變數y的值就是12 × 2,即24。隨後的yield(y / 3)(即yield(24 / 3))返回值8。然後第三個next(13)13作為參數傳入,用來代替表達式yield(y / 3),所以變數z的值是13

  最後,語句return (x + y + z)return (5 + 24 + 13),所以最終的返回值是42

  多重溫幾次上面的代碼,開始的時候你會覺得很難懂,只要理解了generator函數執行的過程,掌握起來並不難。

 

for..of迴圈

  ES6還從語法層面上對遍歷器提供了直接的支持,即for..of迴圈。看下麵的例子:

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

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

console.log( v ); // 仍然是5,而不是6

  正如你所看到的,由foo()創建的遍歷器被for..of迴圈自動捕獲,然後自動進行遍歷,每遍歷一次就返回一個值,直到屬性done的值為true。只要屬性done的值為false,它就會自動提取value屬性的值並將其傳遞給迭代變數(本例中為變數v)。一旦屬性done的值為true,迴圈遍歷就停止(而且不會包含函數的返回值,如果有的話。所以此處的return 6不包括在for..of迴圈中)。

  如上所述,可以看到for..of迴圈忽略並拋棄了返回值6,這是因為此處沒有對應的next()方法被調用,for..of迴圈不支持將值傳遞給generator函數迭代的情況,如在for..of迴圈中使用next(v)。事實上,在使用for..of迴圈時不需要使用next方法。

 

總結

  以上就是generator函數的基本概念。如果你仍然覺得有點難以理解,也不用太擔心,任何人剛開始接觸generator函數時都會有這種感覺!

  你應該會很自然地想到generator函數能在自己的代碼中起到什麼樣的作用,儘管我們會在很多地方用到它。我們剛剛只是接觸到了一些皮毛,還有很多需要瞭解的,所以我們必須深入研究,才能發現它是如此的強大。

  嘗試在Chrome nightly/canary或FireFox nightly或node 0.11+(使用--harmony參數)環境中運行本文的示例代碼,並思考下麵的問題:

  1. 如何處理異常?
  2. 在一個generator函數中可以調用另一個generator函數嗎?
  3. 如何在generator函數中進行非同步編程?

  接下來的文章會解答上述問題,並繼續深入探討有關ES6 generator函數的內容,敬請關註!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 中間件express.static 我們使用express初始化一個目錄的時候,會在app.js中看到一大推的app.use。 其中一個主要的中間件是 express.static (4.0版本依舊保留的中間件) 通過 express.static 可以幫助我們 托管 靜態文件,js,css,img ...
  • 由OpenDigg 出品的前端開源項目周報第七期來啦。我們的前端開源周報集合了OpenDigg一周來新收錄的優質的前端開源項目,方便前端開發人員便捷的找到自己需要的項目工具等。lottie-react-native 實時渲染After Effects動畫 react-navigation 學習一次隨 ...
  • 寫在前面的話 過去一年自己接觸了不少手機前端開發,得益於現在手機性能的提升和4G普及,感覺使用混合技術開發手機App已經可以滿足越來越多的應用場景了。新年伊始,對自己2016年所學知識做一個階段性總結,記錄一個自製的“小而萌”Hybrid App開發歷程,共勉! 〖擴展閱讀〗 企業移動應用開發 混合 ...
  • 每個Nodejs項目的根目錄下麵,一般都會有一個package.json文件。該文件可以由 生成,定義了項目所需要的各種模塊,以及項目的配置信息(比如名稱、版本、許可證等元數據)。 package.json文件內部就是一個JSON對象,該對象的每一個成員就是當前項目的一項設置。 基本欄位 1. na ...
  • 一、relative對absolute的限製作用 1、限制left/top/right/bottom定位。absolute預設是在也沒的左上角,當父類設定為relative,absolute就被限制在父類的區域內,設定top/left/right/bottom時,起始點為父類的左上角 2、限制z-i ...
  • v-if v-if指令可以完全根據表達式的值在DOM中生成或移除一個元素。如果v-if表達式賦值為false,那麼對應的元素就會從DOM中移除;否則,對應元素的一個克隆將被重新插入DOM中,代碼如下: 由於v-if是一個指令,需要將它添加到一個元素上。但是如果想要切換多個元素,則可以把<templa ...
  • 一、六種數據類型 (弱類型數據) 1.基本數據類型(5種):Undefined、Null、Boolean、Number、String 2.引用數據類型(1種):Object 例如: Function Date Array ... 在js中定義變數的時候無需指定類型。比如定義一個變數 var num ...
  • Open and modern framework for building user interfaces. Omi的Github地址 "https://github.com/AlloyTeam/omi" 如果想體驗一下Omi框架,請 "點擊Omi Playground" 如果想使用Omi框架,請 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...