ES6中的Generator

ES6中引入不少新特性,其中關於異步操做的處理就引入了Promise和生成器。衆所周知,Promise能夠在必定程度上解決被廣爲詬病的回調地獄問題。可是在處理多個異步操做時採用Promise鏈式調用的語法也會顯得不是那麼優雅和直觀。而生成器在Promise的基礎上更進一步,容許咱們用同步的方式來描述咱們的異步流程。app

基本介紹

Generator函數和普通函數徹底不一樣,有其不同凡響的獨特語法。一個簡單的Generator函數就長下面這個樣子:異步

function* greet() { yield 'hello' }
複製代碼

在第一次調用Generator函數的時候並不會執行Generator函數內部的代碼,而是會返回一個生成器對象。在前面的文章中,咱們也提過,經過調用這個生成器對象的next函數能夠開始執行Generator函數內部的邏輯,在遇到yield語句會暫停函數的執行,同時向外界返回yield關鍵字後面的結果。暫停以後在須要恢復Generator函數執行時一樣能夠經過調用生成器對象的next方法恢復,同時向next方法傳入的參數會做爲生成器內部當前暫停的yield語句的返回值。如此往復,直到Generator函數內部的代碼執行完畢。舉例:async

function* greet() {
  let result = yield 'hello'
  console.log(result)
}
let g = greet()
g.next() // {value: 'hello', done: false}
g.next(2) // 打印結果爲2,而後返回{value: undefined, done: true}
複製代碼

第一次調用next方法傳入的參數,生成器內部是沒法獲取到的,或者說沒有實際意義,由於此時生成器函數尚未開始執行,第一次調用next方法是用來啓動生成器函數的。函數

yield語法要點

yield 後面能夠是任意合法的JavaScript表達式,yield語句能夠出現的位置能夠等價於通常的賦值表達式(好比a=3)可以出現的位置。舉例:ui

b = 2 + a = 3 // 不合法
b = 2 + (a = 3) // 合法

b = 2 + yield 3 // 不合法
b = 2 + (yield 3) // 合法
複製代碼

yield關鍵字的優先級比較低,幾乎yield以後的任何表達式都會先進行計算,而後再經過yield向外界產生值。並且yield是右結合運算符,也就是說yield yield 123等價於(yield (yield 123))。spa

關於生成器對象

Generator函數返回的生成器對象是Generator函數的一個實例,也就是說返回的生成器對象會繼承Generator函數原型鏈上的方法。舉例:prototype

function* g() {
  yield 1
}
g.prototype.greet = function () {
  console.log('hello')
}
let g1 = g()
console.log(g1 instanceof g) // true
g1.greet() // 'hello'
複製代碼

執行生成器對象的[Symbol.iterator]方法會返回生成器對象自己。代理

function* greet() {}
let g = greet()
console.log(g[Symbol.iterator]() === g) // true
複製代碼

生成器對象還具備如下兩個方法:code

  1. return方法。和迭代器接口的return方法同樣,用於在生成器函數執行過程當中遇到異常或者提早停止(好比在for...of循環中未完成時提早break)時自動調用,同時生成器對象變爲終止態,沒法再繼續產生值。也能夠手動調用來終止迭代器,若是在調用return方法傳入參數,則該參數會做爲最終返回對象的value屬性值。

若是恰好是在生成器函數中的try代碼塊中函數執行暫停而且具備finally代碼塊,此時調用return方法不會當即終止生成器,而是會繼續將finally代碼塊中的邏輯執行完,而後再終止生成器。若是finally代碼塊中包含yield語句,意味着還能夠繼續調用生成器對象的next方法來獲取值,直到finally代碼塊執行結束。舉例:對象

function* ff(){
  yield 1;
  try{ yield 2 }finally{ yield 3 }
}
let fg = ff()
fg.next() // {value: 1, done: false}
fg.return(4) // {value: 4, done: true}
let ffg = ff()
ffg.next() // {value: 1, done: false}
ffg.next() // {value: 2, done: false}
ffg.return(4) // {value: 3, done: false}
ffg.next() // {value: 4, done: true}
複製代碼

從上面的例子中能夠看出,在調用return方法以後若是恰好觸發finally代碼塊而且finally代碼中存在yield語句,就會致使在調用return方法以後生成器對象並不會當即結束,所以在實際使用中不該該在finally代碼塊中使用yield語句。

  1. throw方法。調用此方法會在生成器函數當前暫停執行的位置處拋出一個錯誤。若是生成器函數中沒有對該錯誤進行捕獲,則會致使該生成器對象狀態終止,同時錯誤會從當前throw方法內部向全局傳播。在調用next方法執行生成器函數時,若是生成器函數內部拋出錯誤而沒有被捕獲,也會從next方法內部向全局傳播。

yield*語句

yield* 語句是經過給定的Iterable對象的[Symbol.iterator]方法返回的迭代器來產生值的,也稱爲yield委託,指的是將當前生成器函數產生值的過程委託給了在yield*以後的Iterable對象。基於此,yield* 能夠用來在Generator函數調用另一個Generator函數。舉例:

function* foo() {
  yield 2
  yield 3
  return 4
}
function* bar() {
  let ret = yield* foo()
  console.log(ret) // 4
}
複製代碼

上面的例子中,被代理的Generator函數最終執行完成的返回值最終會做爲代理它的外層Generator函數中yield*語句的返回值。

另外,錯誤也會經過yield*在被委託的生成器函數和控制外部生成器函數的代碼之間傳遞。舉例:

function* delegated() {
  try {
    yield 1
  } catch (e) {
    console.log(e)
  }
  yield 2
  throw "err from delegate"
}

function* delegate() {
  try {
    yield* delegated()
  } catch (e) {
    console.log(e)
  }
  yield 3
}

let d = delegate()
d.next() // {value: 1, done: false}
d.throw('err')
// err
// {value: 2, done: false}
d.next()
// err from delegate
// {value: 3, done: false}
複製代碼

最後須要注意的是yield*和yield之間的區別,容易忽視的一點是yield*並不會中止生成器函數的執行。舉例:

function* foo(x) {
  if (x < 3) {
    x = yield* foo(x + 1)
  }
  return x * 2
}
let f = foo()
f.next() // {value: 24, done: true}
複製代碼

使用Generator組織異步流程

使用Generator函數來處理異步操做的基本思想就是在執行異步操做時暫停生成器函數的執行,而後在階段性異步操做完成的回調中經過生成器對象的next方法讓Generator函數從暫停的位置恢復執行,如此往復直到生成器函數執行結束。

也正是基於這種思想,Generator函數內部才得以將一系列異步操做寫成相似同步操做的形式,形式上更加簡潔明瞭。而要讓Generator函數按順序自動完成內部定義好的一系列異步操做,還須要經過額外的函數來執行Generator函數。對於每次返回值爲非Thunk函數類型的生成器函數,能夠用co模塊來自動執行。而對於遵循callback的異步API,則須要先轉化爲Thunk函數而後再集成到生成器函數中。好比咱們有這樣的API:

logAfterNs = (seconds, callback) => 
    setTimeout(() => {console.log('time out');callback()}, seconds * 1000)
複製代碼

異步流程是這樣的:

logAfterNs(1, function(response_1) {
  logAfterNs(2, function () {
    ...
  })
})
複製代碼

首先咱們須要將異步API轉化爲Thunk形式,也就是原來的API:logAfterNs(...args, callback),咱們須要改造爲:thunkedLogAfterNs(...args)(callback)

function thunkify (fn) {
  return function (...args) {
    return function (callback) {
      args.push(callback)
      return fn.apply(null, args)
    }
  }
}
let thunkedLogAfterNs = thunkify(logAfterNs)
function* sequence() {
  yield thunkedLogAfterNs(1)
  yield thunkedLogAfterNs(2)
}
複製代碼

轉化爲使用生成器函數來改寫咱們的異步流程以後,咱們還須要一個函數來自動管理並執行咱們的生成器函數。

function runTask(gen) {
  let g = gen()
  function next() {
    let result = g.next()
    if (!result.done) result.value(next)
  }
  next()
}

runTask(sequence)
複製代碼

更好的async/await

ES7引入的async/await語法是Generator函數的語法糖,只是前者再也不須要執行器。直接執行async函數就會自動執行函數內部的邏輯。async函數執行結果會返回一個Promise對象,該Promise對象狀態的改變取決於async函數中await語句後面的Promise對象狀態以及async函數最終的返回值。接下來重點講一下async函數中的錯誤處理。

await關鍵字以後能夠是Promise對象,也能夠是原始類型值。若是是Promise對象,則將Promise對象的完成值做爲await語句的返回值,一旦其中有Promise對象轉化爲Rejected狀態,async函數返回的Promise對象也會隨之轉化爲Rejected狀態。舉例:

async function aa() {await Promise.reject('error!')}
aa().then(() => console.log('resolved'), e => console.error(e)) // error!
複製代碼

若是await以後的Promise對象轉化爲Rejected,在async函數內部能夠經過try...catch捕獲到對應的錯誤。舉例:

async function aa() {
  try {
    await Promise.reject('error!')
  } catch(e) {
    console.log(e)
  }
}
aa().then(() => console.log('resolved'), e => console.error(e))
// error!
// resolved
複製代碼

若是async函數中沒有對轉化爲Rejected狀態的Promise進行捕獲,則在外層對調用aa函數進行捕獲並不能捕獲到錯誤,而是會把aa函數返回的Promise對象轉化爲Rejected狀態,在前一個例子中也說明了這一點。

在實驗中還嘗試使用函數對象做爲await關鍵字以後的值,結果發現await在遇到這種狀況時也是按照普通值進行處理,就是await表達式的結果就是該函數對象。

async function bb(){
  let result = await (() => {}); 
  console.log(result);
  return 'done'
}
bb().then(r => console.log(r), e => console.log(e))
// () => {}
// done
複製代碼

總結

文章中咱們介紹了Generator函數的基本用法和注意事項,而且也舉了一個實際的例子來講明如何使用Generator函數來描述咱們的異步流程,最後還簡單介紹了async函數的使用。總而言之,ES6以後也提供了更多管理異步流程的方式,使得咱們的代碼組織起來更加清晰,更加高效!

相關文章
相關標籤/搜索