一次搞懂 Generator 函數

一、什麼是 Generator 函數ajax

在Javascript中,一個函數一旦開始執行,就會運行到最後或遇到return時結束,運行期間不會有其它代碼可以打斷它,也不能從外部再傳入值到函數體內express

而Generator函數(生成器)的出現使得打破函數的完整運行成爲了可能,其語法行爲與傳統函數徹底不一樣編程

Generator函數是ES6提供的一種異步編程解決方案,形式上也是一個普通函數,但有幾個顯著的特徵:
 
  --  function關鍵字與函數名之間有一個星號 "*" (推薦緊挨着function關鍵字)
  --  函數體內使用 yield 表達式,定義不一樣的內部狀態 (能夠有多個yield)
  --  直接調用 Generator函數並不會執行,也不會返回運行結果,而是返回一個遍歷器對象(Iterator Object)
  --  依次調用遍歷器對象的next方法,遍歷 Generator函數內部的每個狀態
 
{
  // 傳統函數
  function foo() {
    return 'hello world'
  }

  foo()   // 'hello world',一旦調用當即執行


  // Generator函數
  function* generator() {
    yield 'status one'         // yield 表達式是暫停執行的標記  
    return 'hello world'
  }

  let iterator = generator()   // 調用 Generator函數,函數並無執行,返回的是一個Iterator對象
  iterator.next()              // {value: "status one", done: false},value 表示返回值,done 表示遍歷尚未結束
  iterator.next()              // {value: "hello world", done: true},value 表示返回值,done 表示遍歷結束
}
 
上面的代碼中能夠看到傳統函數和Generator函數的運行是徹底不一樣的,傳統函數調用後當即執行並輸出了返回值;Generator函數則沒有執行而是返回一個Iterator對象,並經過調用Iterator對象的next方法來遍歷,函數體內的執行看起來更像是「被人踢一腳才動一下」的感受
 
{
  function* gen() {
    yield 'hello'
    yield 'world'
    return 'ending'
  }

  let it = gen()

  it.next()   // {value: "hello", done: false}
  it.next()   // {value: "world", done: false}
  it.next()   // {value: "ending", done: true}
  it.next()   // {value: undefined, done: true}
}
 
上面代碼中定義了一個 Generator函數,其中包含兩個 yield 表達式和一個 return 語句(即產生了三個狀態)
 
每次調用Iterator對象的next方法時,內部的指針就會從函數的頭部或上一次停下來的地方開始執行,直到遇到下一個 yield 表達式或return語句暫停。換句話說,Generator 函數是分段執行的,yield表達式是暫停執行的標記,而 next方法能夠恢復執行
 
執行過程以下:
 
第一次調用next方法時,內部指針從函數頭部開始執行,遇到第一個 yield 表達式暫停,並返回當前狀態的值 'hello'
 
第二次調用next方法時,內部指針從上一個(即第一個) yield 表達式開始,遇到第二個 yield 表達式暫停,返回當前狀態的值 'world'
 
第三次調用next方法時,內部指針從第二個 yield 表達式開始,遇到return語句暫停,返回當前狀態的值 'end',同時全部狀態遍歷完畢,done 屬性的值變爲true
 
第四次調用next方法時,因爲函數已經遍歷運行完畢,再也不有其它狀態,所以返回 {value: undefined, done: true}。若是繼續調用next方法,返回的也都是這個值
 
 
二、yield 表達式
 
(1)、yield 表達式只能用在 Generator 函數裏面,用在其它地方都會報錯
{
  (function (){
    yield 1;
  })()

  // SyntaxError: Unexpected number
  // 在一個普通函數中使用yield表達式,結果產生一個句法錯誤
}

 

(2)、yield 表達式若是用在另外一個表達式中,必須放在圓括號裏面
{
  function* demo() {
    console.log('Hello' + yield); // SyntaxError
    console.log('Hello' + yield 123); // SyntaxError
  
    console.log('Hello' + (yield)); // OK
    console.log('Hello' + (yield 123)); // OK
  }
}

 

(3)、yield 表達式用做參數或放在賦值表達式的右邊,能夠不加括號
{
  function* demo() {
    foo(yield 'a', yield 'b'); // OK
    let input = yield; // OK
  }
}

 

(4)、yield 表達式和return語句的區別
   類似:都能返回緊跟在語句後面的那個表達式的值
   區別:
     --  每次遇到 yield,函數就暫停執行,下一次再從該位置繼續向後執行;而 return 語句不具有記憶位置的功能
     --  一個函數只能執行一次 return 語句,而在 Generator 函數中能夠有任意多個 yield
 
 
三、yield* 表達式
 
若是在 Generator 函數裏面調用另外一個 Generator 函數,默認狀況下是沒有效果的
 
{
  function* foo() {
    yield 'aaa'
    yield 'bbb'
  }

  function* bar() {
    foo()
    yield 'ccc'
    yield 'ddd'
  }

  let iterator = bar()

  for(let value of iterator) {
    console.log(value)
  }

  // ccc
  // ddd

}

 

上例中,使用 for...of 來遍歷函數bar的生成的遍歷器對象時,只返回了bar自身的兩個狀態值。此時,若是想要正確的在bar 裏調用foo,就須要用到 yield* 表達式
 
yield* 表達式用來在一個 Generator 函數裏面 執行 另外一個 Generator 函數
 
{
  function* foo() {
    yield 'aaa'
    yield 'bbb'
  }

  function* bar() {
    yield* foo()      // 在bar函數中 **執行** foo函數
    yield 'ccc'
    yield 'ddd'
  }

  let iterator = bar()

  for(let value of iterator) {
    console.log(value)
  }

  // aaa
  // bbb
  // ccc
  // ddd
}

 

 四、next() 方法的參數
 
yield表達式自己沒有返回值,或者說老是返回undefined。next方法能夠帶一個參數,該參數就會被看成上一個yield表達式的返回值
 
  [rv] = yield [expression]

  expression:定義經過遍歷器從生成器函數返回的值,若是省略,則返回 undefined
  rv:接收從下一個 next() 方法傳遞來的參數

 

先看一個簡單的小栗子,並嘗試解析遍歷生成器函數的執行過程
 
{
  function* gen() {
    let result = yield 3 + 5 + 6
    console.log(result)
    yield result
  }

  let it = gen()
  console.log(it.next())      // {value: 14, done: false}
  console.log(it.next())      // undefined    {value: undefined, done: false}
}

 

(此處分析過程純屬我的理解,有誤之處,歡迎批評指正!)
第一次調用遍歷器對象的next方法,函數從頭部開始執行,遇到第一個 yield 暫停,在這個過程當中實際上是分了三步:
 
(1)、聲明瞭一個變量result,並將聲明提早,默認值爲 undefined
(2)、因爲 Generator函數是 「 惰性求值」,執行到第一個 yield 時纔會計算求和,並加計算結果返回給遍歷器對象 {value: 14, done: false},函數暫停運行
(3)、理論上應該要把等號右邊的 [yield 3 + 5 + 6] 賦值給變量result,可是,因爲函數執行到 yield 時暫定了,這一步就被掛起了
 
第二次調用next方法,函數從上一次 yield 停下的地方開始執行,也就是給result賦值的地方開始,因爲next()並無傳參,就至關於傳參爲undefined
 
基於以上分析,就不難理解爲何說 yield 表達式自己的返回值(特指 [rv])老是undefined了。如今把上面的代碼稍做修改,第二次調用 next() 方法傳一個參數3,按照上圖分析能夠很快得出輸出結果
 
{
  function* gen() {
    let result = yield 3 + 5 + 6
    console.log(result)
    yield result
  }

  let it = gen()
  console.log(it.next())      // {value: 14, done: false}
  console.log(it.next(3))      // 3    {value: 3, done: false}
}

 

若是第一次調用next()的時候也傳了一個參數呢?這個固然是無效的,next方法的參數表示上一個yield表達式的返回值,因此在第一次使用next方法時,傳遞參數是無效的。
 
從語義上講,第一個next方法用來啓動遍歷器對象,因此不用帶有參數。
 
{
  function* gen() {
    let result = yield 3 + 5 + 6
    console.log(result)
    yield result
  }

  let it = gen()
  console.log(it.next(10))      // {value: 14, done: false}
  console.log(it.next(3))      // 3    {value: 3, done: false}
}

 

Generator 函數從暫停狀態到恢復運行,它的上下文狀態(context)是不變的。經過next方法的參數,就有辦法在 Generator 函數開始運行以後,繼續向函數體內部注入值。也就是說,能夠在 Generator 函數運行的不一樣階段,從外部向內部注入不一樣的值,從而調整函數行爲。
 
{
  function* gen(x) {
    let y = 2 * (yield (x + 1))   // 注意:yield 表達式若是用在另外一個表達式中,必須放在圓括號裏面
    let z = yield (y / 3)
    return x + y + z
  }

  let it = gen(5)
  /* 經過前面的介紹就知道這部分輸出結果是錯誤的啦
    
    console.log(it.next())  // {value: 6, done: false}
    console.log(it.next())  // {value: 2, done: false}
    console.log(it.next())  // {value: 13, done: false}
  */
  
  /*** 正確的結果在這裏 ***/
  console.log(it.next())  // 首次調用next,函數只會執行到 「yield(5+1)」 暫停,並返回 {value: 6, done: false}
  console.log(it.next())  // 第二次調用next,沒有傳遞參數,因此 y的值是undefined,那麼 y/3 固然是一個NaN,因此應該返回 {value: NaN, done: false}
  console.log(it.next())  // 一樣的道理,z也是undefined,6 + undefined + undefined = NaN,返回 {value: NaN, done: true}
}

 

若是向next方法提供參數,返回結果就徹底不同了
 
{
  function* gen(x) {
    let y = 2 * (yield (x + 1))   // 注意:yield 表達式若是用在另外一個表達式中,必須放在圓括號裏面
    let z = yield (y / 3)
    return x + y + z
  }

  let it = gen(5)

  console.log(it.next())  // 正常的運算應該是先執行圓括號內的計算,再去乘以2,因爲圓括號內被 yield 返回 5 + 1 的結果並暫停,因此返回{value: 6, done: false}
  console.log(it.next(9))  // 上次是在圓括號內部暫停的,因此第二次調用 next方法應該從圓括號裏面開始,就變成了 let y = 2 * (9),y被賦值爲18,因此第二次返回的應該是 18/3的結果 {value: 6, done: false}
  console.log(it.next(2))  // 參數2被賦值給了 z,最終 x + y + z = 5 + 18 + 2 = 25,返回 {value: 25, done: true}
}
{
  function* gen(x) {
    let y = 2 * (yield (x + 1))   
    let z = yield (y / 3)
    z = 88    // 注意看這裏
    return x + y + z
  }

  let it = gen(5)

  console.log(it.next())   // {value: 6, done: false}
  console.log(it.next(9))  // {value: 6, done: false}
  console.log(it.next(2))  // 這裏其實也很容易理解,參數2被賦值給了 z,可是函數體內又給 z 從新賦值爲88, 最終 x + y + z = 5 + 18 + 88 = 111,返回 {value: 111, done: true}
}

 

五、與 Iterator 接口的關係json

 

ES6 規定,默認的 Iterator 接口部署在數據結構的Symbol.iterator屬性,或者說,一個數據結構只要具備Symbol.iterator屬性,就能夠認爲是「可遍歷的」(iterable)。

Symbol.iterator屬性自己是一個函數,就是當前數據結構默認的遍歷器生成函數。執行這個函數,就會返回一個遍歷器。

因爲執行 Generator 函數實際返回的是一個遍歷器,所以能夠把 Generator 賦值給對象的Symbol.iterator屬性,從而使得該對象具備 Iterator 接口。
 
{
  let obj = {}

  function* gen() {
    yield 4
    yield 5
    yield 6
  }

  obj[Symbol.iterator] = gen

  for(let value of obj) {
    console.log(value)
  }
  // 4
  // 5
  // 6

  console.log([...obj])    // [4, 5, 6]
}
 
傳統對象沒有原生部署 Iterator接口,不能使用 for...of 和 擴展運算符,如今經過給對象添加 Symbol.iterator 屬性和對應的遍歷器生成函數,就可使用了
 
 
六、for...of 循環
 
因爲 Generator 函數運行時生成的是一個 Iterator 對象,所以,能夠直接使用 for...of 循環遍歷,且此時無需再調用 next() 方法
 
這裏須要注意,一旦 next() 方法的返回對象的 done 屬性爲 true,for...of 循環就會終止,且不包含該返回對象
 
{
  function* gen() {
    yield 1
    yield 2
    yield 3
    yield 4
    return 5
  }
  
  for(let item of gen()) {
    console.log(item)
  }
  
  // 1 2 3 4
}
 
 
七、Generator.prototype.return()
 
Generator 函數返回的遍歷器對象,還有一個 return 方法,能夠返回給定的值(若沒有提供參數,則返回值的value屬性爲 undefined),而且 **終結**遍歷 Generator 函數
 
{
  function* gen() {
    yield 1
    yield 2
    yield 3
  }

  let it = gen()

  it.next()             // {value: 1, done: false}
  it.return('ending')   // {value: "ending", done: true}
  it.next()             // {value: undefined, done: true}
}

 

  Generator 函數應用舉例
 
應用一:假定某公司的年會上有一個抽獎活動,總共6我的能夠抽6次,每抽一次,抽獎機會就會遞減
 
按照常規作法就須要聲明一個全局的變量來保存剩餘的可抽獎次數,而全局變量會形成全局污染,指不定何時就被從新賦值了,因此每每並不被你們推薦
 
{
  let count = 6  // 聲明一個全局變量

  // 具體抽獎邏輯的方法
  function draw() {
    // 執行一段抽獎邏輯
    // ...
    // 執行完畢

    console.log(`剩餘${count}次`)
  }

  // 執行抽獎的方法
  function startDrawing(){
    if(count > 0) {
      count--
      draw(count)
    }
  }


  let btn = document.createElement('button')
  btn.id = 'start'
  btn.textContent = '開始抽獎'
  document.body.appendChild(btn)

  document.getElementById('start').addEventListener('click', function(){
    startDrawing()
  }, false)

}

事實上,抽獎一般是每一個人本身來抽,每抽一次就調用一次抽獎方法,而不是點一次就一次性就所有運行完,是可暫停的,這個不就是 Generator 函數的意義所在嗎?
 
{
  // 具體抽獎邏輯的方法
  function draw(count) {
    // 執行一段抽獎邏輯
    // ...

    console.log(`剩餘${count}次`)
  }

  // 執行抽獎的方法
  function* remain(count) {
    while(count > 0) {
      count--
      yield draw(count)
    }
  }

  let startDrawing = remain(6)

  let btn = document.createElement('button')
  btn.id = 'start'
  btn.textContent = '開始抽獎'
  document.body.appendChild(btn)

  document.getElementById('start').addEventListener('click', function(){
    startDrawing.next()
  }, false)
}

 

應用二:因爲HTTP是一種無狀態協議,執行一次請求後服務器沒法記住是從哪一個客戶端發起的請求,所以當須要實時把服務器數據更新到客戶端時一般採用的方法是長輪詢和Websocket。這裏也能夠用 Generator 函數來實現長輪詢
 
{
  // 請求的方法
  function* ajax() {
    yield new Promise((resolve, reject) => {
      // 此處用一個定時器來模擬請求數據的耗時,並約定當返回的json中code爲0表示有新數據更新
      setTimeout(() => {
        resolve({code: 0})
      }, 200)
    })
  }

  // 長輪詢的方法
  function update() {
    let promise = ajax().next().value    // 返回的對象的value屬性是一個 Promise 實例對象
    promise.then(res => {
      if(res.code != 0) {
        setTimeout(() => {
          console.log('2秒後繼續查詢.....')
          update()
        }, 2000)
      } else{
        console.log(res)
      }
    })
  }

  update()
}
相關文章
相關標籤/搜索