ES6 Features系列:GeneratorFunction介紹

1、前言                           

第一次看koajs的示例時,發現該語句 function *(next){...............} ,這是啥啊?因而搜索一下,原來這是就是ES6的新特性Generator Function(生成器函數)。javascript

那什麼是生成器函數呢?其實就至關於C#2.0中經過yield關鍵字實現的迭代器的生成器(細節有所不一樣),那麼理解的關鍵就在迭代器和yield關鍵字兩部分了。下面將嘗試從表象出發,逐步對生成器函數及利用它進行異步編程進行淺層的分析理解。html

2、表象——語法及基本使用                   

示例:java

// 定義生成器函數
function *enumerable(msg){
  console.log(msg)
  var msg1 = yield msg + '  after '
  console.log(msg1)
  var msg2 = yield msg1 + ' after'
  try{
    var msg3 = yield msg2 + 'after'
    console.log('ok')
  }
  catch(e){
    console.log(e)
  }
  console.log(msg2 + ' over')
}

// 初始化迭代器
var enumerator = enumerable('hello')
var ret = enumerator.next() // 控制檯顯示 hello,ret的值{value:'hello after',done:false}
ret =  enumerator.next('world') // 控制檯顯示 world,ret的值{value:'world after',done:false}
ret = enumerator.next('game') // 控制檯顯示game,ret的值{value:'game after',done:false}
// 拋出異常信息
ret = enumerator.throw(new Error('test')) // 控制檯顯示new Error('test')信息,而後顯示game over。ret的值爲{done:true}

// for...of語句
enumerator = enumerable('hello')
for(ret of enumerator)
  console.log(JSON.stringify(ret));
// 控制檯依次顯示
// hello
// {value:'hello after',done:false}
// world
// {value:'world after',done:false}
// {value:'game after',done:false}
// game over
// {done:true}

1. 生成器語函數定義

function* test(){}
function * test(){}
function *test(){}
test = function* (){} 
test = function *(){}

普通函數添加*號後則成爲了成爲了生成器函數了。

Object.prototype.toString.call(test) // 顯示[object GeneratorFunction]
  生成器函數的行爲與普通函數並不相同,表現爲以下3點:
  1. 經過new運算符或函數調用的形式調用生成器函數,均會返回一個生成器實例;
  2. 經過new運算符或函數調用的形式調用生成器函數,均不會立刻執行函數體的代碼;
  3. 必須調用生成器實例的next方法纔會執行生成器函數體的代碼。

function *say(msg){
  console.log(msg)
}
var gen = say('hello world') // 沒有顯示hello world
console.log(Object.prototype.toString.call(gen)) // 顯示[object Generator]
gen.next() // 顯示hello world

二、 關鍵字yield——迭代器生成器

用於立刻退出代碼塊並保留現場,當執行迭代器的next函數時,則能從退出點恢復現場並繼續執行下去。下面有2點須要注意:
1. yield後面的表達式將做爲迭代器next函數的返回值;
2. 迭代器next函數的入參將做爲yield的返回值(有點像運算符)。
三、迭代器(Generator)
迭代器是一個擁有 {value:{}, done:{Boolean}} next([])方法 和 {undefined} throw([*])方法 的對象,經過next函數不斷執行以關鍵字yield分割的代碼段,經過throw函數令yield分割的代碼段拋出異常。git

3、核心1——迭代器                     

迭代器更多的是指迭代器模式,迭代器模式是指經過一個名爲迭代器的對象按必定的規則遍歷集合元素,調用者只需告訴迭代器獲取下一個元素便可,而集合的類型、如何獲取元素等因素均由具體的迭代器自行處理。(又一次地關注點分離!)而且因爲迭代器模式能夠作到 按需執行/延遲執行 的效果,所以能下降遍歷無限序列時內存/棧溢出的問題,也能做爲異步編程模式使用。
模式理解的注意點:
1. 迭代器每次進訪問集合的一個元素,並由調用者發起訪問請求時迭代器才執行下一次訪問操做
2. 「按必定的規則」,意味着不必定遍歷集合中全部的元素,而且規則能夠內聚到迭代器的具體實現上,也可經過策略模式外移到其餘模塊中;
3. 「集合」,集合能夠是一開始就已經初始化好的有限序列集合(如[1,2,3,4,5,6,7]),也能夠是按需生成的無限序列集合(如1到無限大)
4. 「集合元素」,能夠是整數集合、字符串集合等數據集合,也能夠是函數等指令+數據的集合;
若觸過C#、Java等服務端語句的朋友應該對迭代器有必定程度的瞭解,C#的IEnumrable、IEnumerator和Java的Iterable、Iterator就是跟迭代器相關的接口定義,繼承上述接口的迭代器實現都可以經過foreach或for...in語句做循環操做。es6

那麼這裏有2點是要注意的:
1. 迭代器是指設計模式,跟具體的語言無關,所以全部語言都可根據該模式實現具體的迭代器;
2. foreach或for...in語句是語法層面的支持,跟迭代器模式沒有必然聯繫。(若語法層面不支持,那函數式編程中的遞歸的效果是同樣的,假如編譯器/解析器支持尾遞歸則更好了,能夠JS不支持)
下面咱們經過迭代器來實現Python中的range函數,並經過range函數建立一個超大的有限序列正整數集合(直接用數組的話絕有可能致使棧溢出哦!)。github

// 迭代器構造函數
var RangeIterator = function(start,end,scan){
    this.start = arguments.length >= 2 ? start : 0    
    this.end = end == undefined ? start : end
    this.scan = scan || 1
    this.idx = this.start
}
// 向迭代器發起訪問下一個元素的請求
// FF和ES6下迭代器接口規範定義了迭代器必須經過名爲next的函數發起訪問下一個元素的請求
RangeIterator.prototype.next = function(){
    if (this.idx > this.end) 
    if (!!StopIteration) {
         throw StopIteration
       }else{
          return void 0
       }

    var ret = this.idx
    this.idx += this.scan
    return ret
}
// Python中的range函數
var range = function(start, end, scan){
   var iterator = new RangeIterator(start, end, scan)
   return {
       // FF下令for...in語句調用對象的迭代器的接口規範
        __iterator__: function(){
            return iterator
        },
       // 暴露迭代器的next函數
        next: function(){
            return iterator.next()
        },
        toString: function(){
            // 可能會致使棧溢出
            var array = []
            for (var i = this.next(); i != void 0; i = this.next())
                array.push(i)
            return array + ''
        }    
    }
}
var r = range(1, 100000000000000000000)
// FF下
// 參考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Iterators_and_Generators#.E5.AE.9A.E4.B9.89.E8.87.AA.E5.AE.9A.E4.B9.89.E8.BF.AD.E4.BB.A3.E5.99.A8
for(var i in r)
  console.log(i) // 顯示1到99999999999999999999
// 全部瀏覽器
for (var i = r.next(); i != void 0; i = r.next())
  console.log(i) // 顯示1到99999999999999999999

因爲JS是單線程運行,而且當UI線程被阻塞N秒後,瀏覽器會詢問是否中止腳本的執行,但上述代碼並不會因爲序列過大形成棧溢出的問題。假如預先生成1到99999999999999999999或更大數字的數組,那頗有可能形成stack overflow。那是因爲迭代器實質爲一狀態機,而調用next函數則是觸發狀態的轉換,而狀態機中同一時刻用於存放變量的存儲空間固定,並不會出現無限增加的狀況。編程

4、核心2——yield關鍵字                  

回到關鍵字yield上了,其實yield關鍵字就是以一種更直觀、便捷的方式讓咱們建立用於遍歷有限序列集合的迭代器,而yield則用於將生成器函數的代碼切片做爲有限序列集合的元素(元素的類型爲指令+數據,而不只僅是數據而已)。下面咱們一塊兒看看yield關鍵字是怎樣對代碼切片的吧!json

// 定義生成器函數
function *enumerable(msg){
  console.log(msg)
  var msg1 = yield msg + '  after '
  console.log(msg1)
  var msg2 = yield msg1 + ' after'
  console.log(msg2 + ' over')
}

上述代碼最終會被解析爲下面的代碼:設計模式

var enumerable = function(msg){
  var state = -1

  return {
    next: function(val){
      switch(++state){
         case 0:
                  console.log(msg + ' after')
                  break
         case 1:
                  var msg1 = val
                  console.log(msg1 + ' after')
                  break
         case 2:
                  var msg2 = val
                  console.log(msg2 + ' over')
                  break
      }
    }
  }
}

(注意:上述僅僅簡單的分析,更復雜的狀況(條件控制、循環、迭代、異常捕獲處理等)能夠參考@趙劼的《人肉反編譯使用關鍵字yield的方法》)數組

5、異步調用中的應用                   

因爲迭代器模式實現 延遲執行/按需執行,所以可做爲一種異步編程模式來應用。

var iterator = getArticles('dummy.json')
// 開始執行
iterator.next()
// 異步任務模型
function getData(src){
  setTimeout(function(){
    iterator.next({tpl: 'tpl.html', name: 'fsjohnhuang'})
  }, 1000)
}
function getTpl(tpl){
  setTimeout(function(){
    iterator.next('hello ${name}')
  }, 3000)
}
// 同步任務
function render(data, tpl){
  return tpl.replace(/\$\{(\w+)\}/, function(){
    return data[arguments[1]] ==  void 0 ? arguments[0] : data[arguments[1]]
  })
}

// 主邏輯
function *getAritcles(src){
  console.log('begin')
  var data = yield getData(src)
  var tpl = yield getTpl(data.tpl)
  var res = render(data, tpl)
  console.log(rest)
}

主邏輯中異步調用的寫法與同步調用的基本沒差別了,爽了吧!但異步任務模型與生成器函數及其生成的迭代器耦合性太大,仍是不太好用。下面咱們經過實現了Promises/A+規範的Q來進一步解耦。

若執行引擎不支持關鍵字yield,那麼上述代碼不就沒法執行了嗎?仍是那句話,yield關鍵字其實就是語法糖,最終仍是會被解析爲一個迭代器。所以咱們自行實現一個迭代器也是能實現上述效果的,不過過程會繁瑣不少(若如第2節的示例那樣存在try...catch語句,就繁瑣死了@~@),而且代碼的整潔性、可維護性就全靠攻城獅來保證了。(語法糖從語法層面簡化編程和維護難度,但理解底層的工做原理也十分重要哦!)

6、與Q結合                        

// 異步任務模型
function getData(src){
  var deferred = Q.defer()
  setTimeout(function(){
   defer.resolve({tpl: 'tpl.html', name: 'fsjohnhuang'})
  }, 1000)
  return deferred.promise
}
function getTpl(tpl){
  var deferred = Q.defer()
  setTimeout(function(){
   defer.resolve('hello ${name}')
  }, 3000)
  return deferred.promise
}
// 同步任務
function render(data, tpl){
  return tpl.replace(/\$\{(\w+)\}/, function(){
    return data[arguments[1]] ==  void 0 ? arguments[0] : data[arguments[1]]
  })
}

// 主邏輯
Q.async(function *(){
  console.log('begin')
  var data = yield getData('dummy.json')
  var tpl = yield getTpl(data.tpl)
  var res = render(data, tpl)
  console.log(rest)
})

暫未閱讀Q的源代碼,暫不做詳細分析。反正API就這樣用,呵呵!

7、與iPromise結合                    

iPromise是我開發的一個Promises/A+的完整實現,閱讀源碼你會發現它繼承了jQuery.Deferred1.5~2.一、jsDeferred、mmDeferred和Promises/A官網實現示例的精妙設計,而且從v0.0.6開始支持ES6特性GeneratorFunction。使用示例以下:

var getData = function(dataSrc){
  return iPromise(function(r){
    setTimeout(function(){
        r(dataSrc + ' has loaded')
    }, 1000)
  })
}
var getTpl = function(tplSrc){
  return iPromise(function(r){
    setTimeout(function(){
        r(tplStr + ' has loaded')
    }, 2000)
  })
}
var render = function(data, tpl){
    throw new Error('OMG!')
}

iPromise(function *(dataSrc, tplSrc){
  try{
    var data = yield getData(dataSrc)
    var tpl = yield getTpl(tplSrc)
    render(data, tpl)
  }
  catch(e){
    console.log(e)
  }
  console.log('over!')
}, 'dummyData.json', 'dummyTpl.json')
/* 結果以下 */
// 等待1秒多顯示 dummyData.json has loaded
// 等待2秒多顯示 dummyTpl.json has loaded
// 顯示 Error: OMG!
//     Stack trace:
//     test10/render/</<@file:///home/fsjohnhuang/repos/iPromise/test/v0.0.2.html:190:6
// 顯示 over!

v0.6.0的中經過遞歸來實現,具體以下(https://github.com/fsjohnhuang/iPromise/blob/master/src/iPromise.js#L7...):

// FF下生成器函數的入參必須在建立迭代器時傳遞
// 若第一次調用迭代器的next函數傳遞參數,則會報TypeError: attempt to send 第一個入參值 to newborn generator
var iterator = mixin.apply(null, toArray(arguments,1))
var next = function(){
  var deferred = iPromise()
  deferred.resolve.apply(deferred, arguments)

  return deferred.then(function(){
    var yieldReturn = iterator.next.apply(iterator, arguments)
     if(yieldReturn.done) throw Error('StopIteration')

     return yieldReturn.value
  }).then(next, function(e){
    iterator.throw(e)
  })
}
deferred.resolve()
deferred.then(next)

8、總結                          

Generator Function並非爲異步編程而生,但能夠將它結合Promise來實現良好的異步編程模型。本篇內容僅簡單介紹Generator Function及相關的異步編程內容,如有紕漏請各位指正,謝謝!

9、 參考                          

http://huangj.in/765
https://www.imququ.com/post/generator-function-in-es6.html
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/The_Iter...
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Stat...*
http://www.cnblogs.com/yangecnu/archive/2012/03/17/2402432.html
http://www.cnblogs.com/draem0507/p/3795189.html
http://blog.zhaojie.me/2010/06/code-for-fun-iterator-generator-yield-i...
http://blog.zhaojie.me/2010/06/code-for-fun-iterator-generator-yield-i...
http://blog.zhaojie.me/2010/07/why-java-sucks-and-csharp-rocks-6-yield... 若是您以爲本文的內容有趣就掃一下吧!捐贈互勉!   

相關文章
相關標籤/搜索