JavaScript >> Javascript 文檔 >  >> JavaScript

了解 JavaScript 中的生成器

這篇文章最初是為 DigitalOcean 寫的。

在 ECMAScript 2015 中,生成器 被引入 JavaScript 語言。生成器是一個可以暫停和恢復並且可以產生多個值的進程。 JavaScript 中的生成器由一個生成器函數組成,它返回一個可迭代的 Generator 對象。

生成器是 JavaScript 的強大補充。它們可以維護狀態,提供一種有效的方法來製作迭代器,並且能夠處理無限的數據流,可用於在 Web 應用程序的前端實現無限滾動,對聲波數據進行操作等等。此外,當與 Promises 一起使用時,生成器可以模仿 async/await 功能,它允許我們以更直接和可讀的方式處理異步代碼。雖然 async/await 生成器是處理常見、簡單的異步用例(例如從 API 獲取數據)的一種更普遍的方法,它具有更高級的功能,因此值得學習如何使用它們。

在本文中,我們將介紹如何創建生成器函數,如何迭代 Generator 對象,yield的區別 和 return 在生成器內部,以及使用生成器的其他方面。

生成器函數

生成器函數是返回 Generator 的函數 對象,由 function 定義 關鍵字後跟一個星號 (* ),如下圖:

// Generator function declaration
function* generatorFunction() {}

有時,您會在函數名稱旁邊看到星號,而不是函數關鍵字,例如 function *generatorFunction() .這工作相同,但 function* 是一種更廣泛接受的語法。

生成器函數也可以像常規函數一樣定義在表達式中:

// Generator function expression
const generatorFunction = function* () {}

生成器甚至可以是對像或類的方法:

// Generator as the method of an object
const generatorObj = {
  *generatorMethod() {},
}

// Generator as the method of a class
class GeneratorClass {
  *generatorMethod() {}
}

本文中的示例將使用生成器函數聲明語法。

現在您知道如何聲明生成器函數,讓我們看看可迭代的 Generator 他們返回的對象。

生成器對象

傳統上,JavaScript 中的函數會運行到完成,調用函數會在到達 return 時返回一個值 關鍵詞。如果 return 省略關鍵字,函數將隱式返回 undefined .

例如,在下面的代碼中,我們聲明了一個 sum() 返回值是兩個整數參數之和的函數:

// A regular function that sums two values
function sum(a, b) {
  return a + b
}

調用函數返回的值是參數的總和:

const value = sum(5, 6) // 11

但是,生成器函數不會立即返回值,而是返回可迭代的 Generator 目的。在下面的例子中,我們聲明一個函數並給它一個返回值,就像一個標準函數:

// Declare a generator function with a single return value
function* generatorFunction() {
  return 'Hello, Generator!'
}

當我們調用生成器函數時,它會返回 Generator 對象,我們可以將其分配給變量:

// Assign the Generator object to generator
const generator = generatorFunction()

如果這是一個常規函數,我們會期望 generator 給我們函數中返回的字符串。然而,我們實際得到的是一個 suspended 中的對象 狀態。調用 generator 因此將給出類似於以下的輸出:

generatorFunction {<suspended>}
  __proto__: Generator
  [[GeneratorLocation]]: VM272:1
  [[GeneratorStatus]]: "suspended"
  [[GeneratorFunction]]: ƒ* generatorFunction()
  [[GeneratorReceiver]]: Window
  [[Scopes]]: Scopes[3]

Generator 函數返回的對像是一個迭代器 .迭代器是具有 next() 的對象 方法可用,用於迭代一系列值。 next() 方法返回一個帶有 value 的對象 和 done 特性。 value 表示返回值,done 指示迭代器是否已遍歷其所有值。

知道了這一點,讓我們調用 next() 在我們的 generator 並獲取迭代器的當前值和狀態:

// Call the next method on the Generator object
generator.next()

這將給出以下輸出:

{value: "Hello, Generator!", done: true}

調用next()返回的值 是 Hello, Generator! ,以及 done 的狀態 是 true , 因為這個值來自 return 關閉了迭代器。由於迭代器完成,生成器函數的狀態將從 suspendedclosed .調用 generator 再次將給出以下內容:

generatorFunction {<closed>}

到目前為止,我們只演示了生成器函數如何成為獲取 return 的更複雜的方法 一個函數的值。但是生成器函數也有區別於普通函數的獨特特性。在下一節中,我們將了解 yield 運算符,看看生成器如何暫停和恢復執行。

yield 運算符

生成器為 JavaScript 引入了一個新關鍵字:yield . yield 可以暫停生成器函數並返回 yield 後面的值 ,提供了一種輕量級的方法來遍歷值。

在這個例子中,我們將使用不同的值暫停生成器函數 3 次,並在最後返回一個值。然後我們將分配我們的 Generator 反對 generator 變量。

// Create a generator function with multiple yields
function* generatorFunction() {
  yield 'Neo'
  yield 'Morpheus'
  yield 'Trinity'

  return 'The Oracle'
}

const generator = generatorFunction()

現在,當我們調用 next() 在生成器函數上,每次遇到yield都會暫停 . done 將設置為 false 在每個 yield 之後 ,表示生成器還沒有完成。一旦遇到 return ,或者沒有更多的 yield 函數中遇到的s,done 將翻轉到 true ,生成器就完成了。

使用 next() 連續四次方法:

// Call next four times
generator.next()
generator.next()
generator.next()
generator.next()

這些將依次給出以下四行輸出:

{value: "Neo", done: false}
{value: "Morpheus", done: false}
{value: "Trinity", done: false}
{value: "The Oracle", done: true}

請注意,生成器不需要 return;如果省略,最後一次迭代將返回 {value: undefined, done: true} ,以及對 next() 的任何後續調用 生成器完成後。

迭代生成器

使用 next() 方法,我們手動遍歷Generator 對象,接收所有 valuedone 完整對象的屬性。但是,就像 Array , Map , 和 Set , 一個 Generator 遵循迭代協議,可以通過for...of進行迭代 :

// Iterate over Generator object
for (const value of generator) {
  console.log(value)
}

這將返回以下內容:

Neo
Morpheus
Trinity

擴展運算符也可用於分配 Generator 的值 到一個數組。

// Create an array from the values of a Generator object
const values = [...generator]

console.log(values)

這將給出以下數組:

(3) ["Neo", "Morpheus", "Trinity"]

傳播和for...of 不會考慮 return 進入值(在這種情況下,它應該是 'The Oracle' )。

關閉一個生成器

正如我們所見,一個生成器可以有它的 done 屬性設置為 true 並將其狀態設置為 closed 通過遍歷它的所有值。還有另外兩種方法可以立即取消生成器:使用 return() 方法,並使用 throw() 方法。

使用 return() ,生成器可以在任何時候終止,就像 return 聲明已經在函數體中。您可以將參數傳遞給 return() , 或者為未定義的值留空。

演示return() ,我們將使用一些 yield 創建一個生成器 值但沒有 return 在函數定義中:

function* generatorFunction() {
  yield 'Neo'
  yield 'Morpheus'
  yield 'Trinity'
}

const generator = generatorFunction()

第一個next() 會給我們'Neo' , 使用 done 設置為 false .如果我們調用 return() Generator 上的方法 對象之後,我們現在將獲取傳遞的值和 done 設置為 true .對 next() 的任何其他調用 將給出一個未定義值的默認完成生成器響應。

為了證明這一點,在 generator 上運行以下三個方法 :

generator.next()
generator.return('There is no spoon!')
generator.next()

這將給出以下三個結果:

{value: "Neo", done: false}
{value: "There is no spoon!", done: true}
{value: undefined, done: true}

return() 方法強制 Generator 要完成並忽略任何其他 yield 的對象 關鍵詞。當您需要使函數可取消時,這在異步編程中特別有用,例如當用戶想要執行不同的操作時中斷 Web 請求,因為無法直接取消 Promise。

如果生成器函數的主體可以捕獲和處理錯誤,則可以使用 throw() 向生成器中拋出錯誤的方法。這會啟動生成器,拋出錯誤,然後終止生成器。

為了證明這一點,我們將放置一個 try...catch 在生成器函數體內,如果發現錯誤,則記錄錯誤:

// Define a generator function
function* generatorFunction() {
  try {
    yield 'Neo'
    yield 'Morpheus'
  } catch (error) {
    console.log(error)
  }
}

// Invoke the generator and throw an error
const generator = generatorFunction()

現在,我們將運行 next() 方法,後跟 throw()

generator.next()
generator.throw(new Error('Agent Smith!'))

這將給出以下輸出:

{value: "Neo", done: false}
Error: Agent Smith!
{value: undefined, done: true}

使用 throw() ,我們在生成器中註入了一個錯誤,該錯誤被 try...catch 捕獲 並登錄到控制台。

生成器對象方法和狀態

下表顯示了可以在 Generator 上使用的方法列表 對象:

方法 說明
next() 返回生成器中的下一個值
return() 在生成器中返回一個值並完成生成器
throw() 拋出錯誤並完成生成器

下表列出了 Generator 的可能狀態 對象:

狀態 說明
suspended 生成器已停止執行但尚未終止
closed 生成器因遇到錯誤、返回或遍歷所有值而終止

yield 委派

除了常規的 yield 運算符,生成器也可以使用 yield* 將更多值委託給另一個生成器的表達式。當 yield* 在生成器中遇到,它將進入委託生成器並開始遍歷所有 yield s 直到該生成器關閉。這可用於分離不同的生成器函數以在語義上組織您的代碼,同時仍然擁有它們的所有 yield 可以按正確的順序迭代。

為了演示,我們可以創建兩個生成器函數,其中一個將 yield* 操作對方:

// Generator function that will be delegated to
function* delegate() {
  yield 3
  yield 4
}

// Outer generator function
function* begin() {
  yield 1
  yield 2
  yield* delegate()
}

接下來,讓我們遍歷 begin() 生成器函數:

// Iterate through the outer generator
const generator = begin()

for (const value of generator) {
  console.log(value)
}

這將按照它們的生成順序給出以下值:

1
2
3
4

外部生成器產生值 12 ,然後使用 yield* 委託給另一個生成器 ,返回 34 .

yield* 也可以委託給任何可迭代的對象,例如 Array 或 Map。收益委派有助於組織代碼,因為生成器中的任何函數都想使用 yield 還必須是生成器。

無限數據流

生成器的有用方面之一是處理無限數據流和集合的能力。這可以通過在生成器函數中創建一個無限循環來證明,該函數將數字加一。

在下面的代碼塊中,我們定義了這個生成器函數,然後啟動生成器:

// Define a generator function that increments by one
function* incrementer() {
  let i = 0

  while (true) {
    yield i++
  }
}

// Initiate the generator
const counter = incrementer()

現在,使用 next() 遍歷這些值 :

// Iterate through the values
counter.next()
counter.next()
counter.next()
counter.next()

這將給出以下輸出:

{value: 0, done: false}
{value: 1, done: false}
{value: 2, done: false}
{value: 3, done: false}

該函數在無限循環中返回連續值,而 done 屬性仍然是 false ,確保它不會完成。

使用生成器,您不必擔心創建無限循環,因為您可以隨意停止和恢復執行。但是,您仍然必須小心調用生成器的方式。如果您使用價差或 for...of 在無限數據流上,您仍然會一次遍歷無限循環,這將導致環境崩潰。

對於無限數據流的更複雜示例,我們可以創建一個斐波那契生成器函數。斐波那契數列將前兩個值連續相加,可以使用生成器中的無限循環編寫如下:

// Create a fibonacci generator function
function* fibonacci() {
  let prev = 0
  let next = 1

  yield prev
  yield next

  // Add previous and next values and yield them forever
  while (true) {
    const newVal = next + prev

    yield newVal

    prev = next
    next = newVal
  }
}

為了測試這一點,我們可以循環一個有限數並將斐波那契數列打印到控制台。

// Print the first 10 values of fibonacci
const fib = fibonacci()

for (let i = 0; i < 10; i++) {
  console.log(fib.next().value)
}

這將給出以下內容:

0
1
1
2
3
5
8
13
21
34

處理無限數據集的能力是生成器如此強大的原因之一。這對於在 Web 應用程序的前端實現無限滾動或對聲波數據進行操作等示例很有用。

在生成器中傳遞值

在整篇文章中,我們使用生成器作為迭代器,並且在每次迭代中都產生了值。除了產生值之外,生成器還可以使用 next() 中的值 .在這種情況下,yield 將包含一個值。

需要注意的是,第一個 next() 被調用的不會傳遞值,而只會啟動生成器。為了證明這一點,我們可以記錄 yield 的值 並調用 next() 有一些值。

function* generatorFunction() {
  console.log(yield)
  console.log(yield)

  return 'The end'
}

const generator = generatorFunction()

generator.next()
generator.next(100)
generator.next(200)

這將給出以下輸出:

100
200
{value: "The end", done: true}

也可以使用初始值播種生成器。在下面的示例中,我們將創建一個 for 循環並將每個值傳遞到 next() 方法,但也將參數傳遞給初始函數:

function* generatorFunction(value) {
  while (true) {
    value = yield value * 10
  }
}

// Initiate a generator and seed it with an initial value
const generator = generatorFunction(0)

for (let i = 0; i < 5; i++) {
  console.log(generator.next(i).value)
}

我們將從 next() 中檢索值 並為下一次迭代產生一個新值,即前一個值乘以 10。這將給出以下內容:

0
10
20
30
40

另一種處理啟動生成器的方法是將生成器包裝在一個始終調用 next() 的函數中 在做任何其他事情之前一次。

async /await 帶發電機

異步函數是 ES6+ JavaScript 中可用的一種函數,它通過使異步數據看起來是同步的,從而使處理異步數據變得更簡單、更容易理解。生成器具有比異步函數更廣泛的功能,但能夠複製類似的行為。以這種方式實現異步編程可以增加代碼的靈活性。

在本節中,我們將演示重現 async 的示例 /await 帶發電機。

讓我們構建一個異步函數,使用 Fetch API 從 JSONPlaceholder API 獲取數據(提供示例 JSON 數據用於測試目的)並將響應記錄到控制台。

首先定義一個名為 getUsers 的異步函數 從 API 獲取數據並返回一個對像數組,然後調用 getUsers

const getUsers = async function () {
  const response = await fetch('https://jsonplaceholder.typicode.com/users')
  const json = await response.json()

  return json
}

// Call the getUsers function and log the response
getUsers().then((response) => console.log(response))

這將給出以下 JSON 數據:


[ {id: 1, name: "Leanne Graham" ...},
  {id: 2, name: "Ervin Howell" ...},
  {id: 3, name": "Clementine Bauch" ...},
  {id: 4, name: "Patricia Lebsack"...},
  {id: 5, name: "Chelsey Dietrich"...},
  {id: 6, name: "Mrs. Dennis Schulist"...},
  {id: 7, name: "Kurtis Weissnat"...},
  {id: 8, name: "Nicholas Runolfsdottir V"...},
  {id: 9, name: "Glenna Reichert"...},
  {id: 10, name: "Clementina DuBuque"...}]

使用生成器,我們可以創建幾乎相同的東西,而不使用 async /await 關鍵詞。相反,它將使用我們創建的新函數和 yield 值而不是 await 承諾。

在下面的代碼塊中,我們定義了一個名為 getUsers 的函數 使用我們新的 asyncAlt 模仿 async 的函數(我們稍後會寫) /await .

const getUsers = asyncAlt(function* () {
  const response = yield fetch('https://jsonplaceholder.typicode.com/users')
  const json = yield response.json()

  return json
})

// Invoking the function
getUsers().then((response) => console.log(response))

如我們所見,它看起來幾乎與 async 相同 /await 實現,只是傳入了一個生成器函數來產生值。

現在我們可以創建一個 asyncAlt 類似於異步函數的函數。 asyncAlt 有一個生成器函數作為參數,這是我們的函數,它產生 fetch 的承諾 返回。 asyncAlt 返回一個函數本身,並解析它找到的每一個promise,直到最後一個:

// Define a function named asyncAlt that takes a generator function as an argument
function asyncAlt(generatorFunction) {
  // Return a function
  return function () {
    // Create and assign the generator object
    const generator = generatorFunction()

    // Define a function that accepts the next iteration of the generator
    function resolve(next) {
      // If the generator is closed and there are no more values to yield,
      // resolve the last value
      if (next.done) {
        return Promise.resolve(next.value)
      }

      // If there are still values to yield, they are promises and
      // must be resolved.
      return Promise.resolve(next.value).then((response) => {
        return resolve(generator.next(response))
      })
    }

    // Begin resolving promises
    return resolve(generator.next())
  }
}

這將提供與 async 相同的輸出 /await 版本:

[ {id: 1, name: "Leanne Graham" ...},
  {id: 2, name: "Ervin Howell" ...},
  {id: 3, name": "Clementine Bauch" ...},
  {id: 4, name: "Patricia Lebsack"...},
  {id: 5, name: "Chelsey Dietrich"...},
  {id: 6, name: "Mrs. Dennis Schulist"...},
  {id: 7, name: "Kurtis Weissnat"...},
  {id: 8, name: "Nicholas Runolfsdottir V"...},
  {id: 9, name: "Glenna Reichert"...},
  {id: 10, name: "Clementina DuBuque"...}]

請注意,此實現用於演示如何使用生成器代替 async /await ,並且不是可生產的設計。它沒有設置錯誤處理,也沒有將參數傳遞給生成值的能力。雖然這種方法可以為您的代碼增加靈活性,但通常是 async/await 將是一個更好的選擇,因為它將實現細節抽像出來,讓您專注於編寫高效的代碼。

結論

生成器是可以暫停和恢復執行的進程。儘管它們並不常用,但它們是 JavaScript 的一個強大、通用的特性。在本教程中,我們了解了生成器函數和生成器對象,生成器可用的方法,yieldyield* 運算符,並使用具有有限和無限數據集的生成器。我們還探索了一種在沒有嵌套回調或長承諾鏈的情況下實現異步代碼的方法。

如果您想了解有關 JavaScript 語法的更多信息,請查看我們的了解 JavaScript 中的 this、Bind、Call 和 Apply 以及了解 JavaScript 中的 Map 和 Set Objects 教程。


Tutorial JavaScript 教程
  1. 在 Next.js 博客中添加分頁

  2. 反應路由器簡介

  3. 使用 AWS CodePipeline 在 Amazon EC2 上部署 NodeJS 應用程序

  4. 令人敬畏的人的令人敬畏的事情 - 2020 年 9 月

  5. 從數組 JavaScript 中刪除前 n 個元素 |示例代碼

  6. JavaScript 浮點精度 |示例代碼

  7. Angular 自定義日期選擇器😱

  1. 什麼觸發 HTML 表單提交?

  2. 如何使用 TDD 和 React 測試庫構建堅固的 React 應用程序

  3. 使用 Reactjs 和 Contentful 創建時間線

  4. Babel 的資助計劃

  5. 使用這些工具提高工作效率! 🌻 我的七月精選給你

  6. react 可以用作實用的全棧 web 應用程序嗎?

  7. 提取狀態下 react-youtube 的播放功能以在按鈕 oncluck 中使用導致 CORS

  1. 使用 vanilla JS 進行分析:頁面瀏覽量

  2. JavaScript-30-Day-6

  3. 如何使用 React Hooks 構建可重用的動畫組件

  4. 天才之路:天才#69