ES6 異步進階第二步:Generator 函數

一、什么是生成器 Generator?

生成器對象是由一個 Generator 函數返回的,并且她符合 可迭代協議和迭代器協議

語法:
function * gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen(); // "Generator { }"

生成器函數有以下四個特征:

  • 在 function 關鍵字和函數名之間多了一個星號
  • 函數內部使用了 yield 表達式,用于定義 Generator 函數中的每個狀態
  • Generator 函數通過多個 yield 表達式定義內部狀態,掉用 Generator 函數時,不會像普通函數會立即執行,而是返回的是一個 Iterator 對象,通過調用 next() 方法,可以依次遍歷 Generator 函數的每個內部狀態
  • Generator 函數的星號的位置有四種情況 如下
function *gen () {}   
function* gen () {}
function * gen () {}
function*gen () {}

一般的寫法是第二種。

二、如何使用 Generator?

1、yield 表達式

yield 關鍵字在 Generator 函數中有兩個作用:定義內部狀態和暫停執行,代碼解釋如下:

function *gen () {
  yield 1
  yield 2
  return 3
}

const g = gen()   // Iterator對象
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: 3, done: true}
g.next() // {value: undefined, done: true}

結合上述代碼和第一節的Generator 函數的特征可知:
在調用 gen 函數時,返回的一個 Iterator 對象,要想獲得每個 yield 表達式的狀態,需要調用 next 方法。
每次調用 next 方法時,都會返回一個包含 value 屬性和 done 屬性的對象,value 屬性表示 yield 表達式的值,done 屬性是一個布爾值,表示遍歷是否結束。
如果 value 沒有返回值或者 返回的是 undefined ,說明是函數運行結束了。
還有一種情況需要注意的額是:yield 表達式 如果用在另一個表達式中,需要為其加上圓括號“()”,作為函數參數和語句時可以不適用圓括號

function *gen () {
  console.log('hello' + yield) ×
  console.log('hello' + (yield)) √
  console.log('hello' + yield '凱斯') ×
  console.log('hello' + (yield '凱斯')) √
  foo(yield 1)  √
  const param = yield 2  √
}
2、yield* 表達式
yield* 表達式的使用場景:在一個 Generator 函數中調用另一個 Generator 函數。下面代碼是不能執行的:
function *foo () {
  yield 1
}
function *gen () {
  foo()
  yield 2
}
const g = gen()
g.next()  // {value: 2, done: false}
g.next()  // {value: undefined, done: true}

如果使用 yield 表達式,value 返回的值是一個遍歷器對象:

function *foo () {
  yield 1
}
function *gen () {
  yield foo()
  yield 2
}
const g = gen()
g.next()   // {value: Generator, done: false}
g.next()   // {value: 2, done: false}
g.next()   // {value: undefined, done: true}

這里要實現上面場景,要使用 yield* 表達式,其實這個表達式就是 for...of 方法的簡寫:

function *foo () {
  yield 1
}
function *gen () {
  yield* foo()
  yield 2
}
const g = gen()
g.next()   // {value: 1, done: false}
g.next()   // {value: 2, done: false}
g.next()   // {value: undefined, done: true}

// 相當于
function *gen () {
  yield 1
  yield 2
}

// 相當于
function *gen () {
  for (let item of foo()) {
    yield item
  }
  yield 2
}

yield* 遍歷具有 Iterator 接口的數據類型:

const arr = ['a', 'b']
const str = 'yuan'
function *gen () {
  yield arr
  yield* arr
  yield str
  yield* str
}
const g = gen()
g.next() // {value: ['a', 'b'], done: false}
g.next() // {value: 'a', done: false}
g.next() // {value: 'b', done: false}
g.next() // {value: 'yuan', done: false}
g.next() // {value: 'y', done: false}
...

使用 yield* 表達式取出嵌套數組中的成員:

// 普通方法
const arr = [1, [[2, 3], 4]]
const str = arr.toString().replace(/,/g, '')
for (let item of str) {
  console.log(+item)      // 1, 2, 3, 4
}

// 使用yield*表達式
function *gen (arr) {
  if (Array.isArray(arr)) {
    for (let i = 0; i < arr.length; i++) {
      yield * gen(arr[i])
    }
  } else {
    yield arr
  }
}
const g = gen([1, [[2, 3], 4]])
for (let item of g) {
  console.log(item)       // 1, 2, 3, 4
}
3、next 方法

next 方法的作用是分階段執行 Generator 函數。
每一次調用next方法,就會從函數頭部或者上一次停下來的地方開始執行,直到遇到下一個yield表達式(return 語句)為止。同時,調用next方法時,會返回包含value和done屬性的對象,value屬性值可以為yield表達式、return語句后面的值或者undefined值,done屬性表示遍歷是否結束。
遍歷器對象 next 方法的執行邏輯如下:

  1. 遇到yield表達式,就暫停執行后面的操作,并將緊跟在yield表達式后面的那個表達式的值,作為返回的對象的value屬性值。
  2. 下一次調用next方法時,再繼續往下執行,直到遇到下一個yield表達式。
  3. 如果沒有再遇到新的yield表達式,就一直運行到函數結束,直到遇到return語句為止,并將return語句后面表達式的值,作為返回的對象的value屬性值。
  4. 如果該函數沒有return語句,則返回的對象的value屬性值為undefined。
function *gen () {
  yield 1
  yield 2
  return 3
}

const g = gen()
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: 3, done: true}
g.next() // {value: undefined, done: true}

根據next運行邏輯再針對這個例子,就很容易理解了。調用gen函數,返回遍歷器對象。
第一次調用next方法時,在遇到第一個yield表達式時停止執行,value屬性值為1,即yield表達式后面的值,done為false表示遍歷沒有結束;
第二次調用next方法時,從暫停的yield表達式后開始執行,直到遇到下一個yield表達式后暫停執行,value屬性值為2,done為false;
第三次調用next方法時,從上一次暫停的yield表達式后開始執行,由于后面沒有yield表達式了,所以遇到return語句時函數執行結束,value屬性值為return語句后面的值,done屬性值為true表示已經遍歷完畢了。
第四次調用next方法時,value屬性值就是undefined了,此時done屬性為true表示遍歷完畢。以后再調用next方法都會是這兩個值。

4、next 方法的參數

yield 語句本身沒有返回值,或者說總是返回 undefined。

function *gen () {
  var x = yield 'hello world'
  var y = x / 2
  return [x, y]
}
const g = gen()
g.next()    // {value: 'hello world', done: false}
g.next()    // {value: [undefined, NaN], done: true}

由上,第二次調用 next 方法時,并沒有返回相應的值,而是返回 undefined,解決方法:給第二次調用的 next 方法傳入一個參數,該參數會當作上一個 yield 語句的返回值

function *gen () {
  var x = yield 'hello world'
  var y = x / 2
  return [x, y]
}
const g = gen()
g.next()    // {value: 'hello world', done: false}
g.next(10)    // {value: [10, 5], done: true}```

注意,這里的 next 方法的參數是指上一個 yield 語句的返回值,所以在第一次調用 next 不能傳入參數。如若要傳參數,需要借用閉包來實現。

function wrapper (gen) {
  return function (...args) {
    const genObj = gen(...args)
    genObj.next()
    return genObj
  }
}
const generator = wrapper(function *generator () {
  console.log(`hello ${yield}`)
  return 'done'
})
const a = generator().next('keith')
console.log(a)   // hello keith, done

實際上,yield 表達式和 next 方法是 generator 函數的雙向信息傳遞。yield 表達式向外傳遞 value 值,next 方法的參數向內傳遞值。

5、遍歷 Generator 對象
與 Iterator 接口的關系

任何一個對象的Symbol.iterator屬性,指向默認的遍歷器對象生成函數。而Generator函數也是遍歷器對象生成函數,所以可以將Generator函數賦值給Symbol.iterator屬性,這樣就使對象具有了Iterator接口。默認情況下,對象是沒有Iterator接口的。
具有Iterator接口的對象,就可以被擴展運算符(...),解構賦值,Array.from和for...of循環遍歷了。

function *gen () {
  yield 1
  yield 2
  yield 3
  return 4
}
for (let item of gen()) {
  console.log(item)  // 1 2 3
}

for...of 循環可以自動遍歷Generator函數生成的Iterator對象,不用調用next方法。

三、應用實例

1、協程

所謂協程:是指多個線程相互協作,完成異步任務。
Generator 函數是協程在 ES6 中的實現,最大特點就是可以交出函數的執行權。
整個Generator 函數就是一個封裝的異步任務,或者說是異步任務的容器。異步操作需要暫停的地方都用 yield 語句注明。Generator 函數實現協程代碼如下:

function* gen() {
  var y = yield x + 2;
  return y;
}
var g = gen(1);
g.next(); // { value: 3, done: false };
g.next(); // { value: undefined; done: true }

2、Thunk 函數

Thunk 函數是自動執行 Generator 函數的一種方法。
Thunk 函數的含義:起源于“傳名調用”的“求職策略”。將參數放到一個臨時函數之中,在將這個臨時參數傳入函數體。這個臨時函數就是 Thunk 函數。
在 JavaScript 語言中,Thunk 函數替換的不是表達式,而是多參數函數,將其替換成一個只接受回調函數作為參數的單參數函數。
任何函數,只要參數有回調函數,都能寫成 Thenk 函數的形式。
Thunk 函數轉換器:

// ES5 版本
var Thunk = function(fn) {
  return function () {
    var args = Array.prototype.slice.call(arguments);
    return function (callback) {
      args.push(callback);
      return fn.apply(this, arg);
    }
  }
};

// ES6 版本
const Thunk = function(fn) {
  return function(...args) {
  return function(callback) {
      return fn.call(this, ...args, callback);
    }
  }
}

使用上面的轉換器,生成, fs.redFile 的 Thunk 函數。

var readFileThunk = Thunk(fs.readFile);
redadFileThunk(fileA)(callback);`

Thunk 函數真正的威力在于可以自動執行 Generator 函數。

function run(fn) {
  var gen = fn();
  function next(err, data) {
    var result = gen.next(data);
    if(result.done) return;
    result.value(next);
  }
  next();
}
function* g() {
  ...
}

run(g);

上面是一個基于 Thunk 函數的 Generator 執行器,可以在generator 函數內部執行多個異步操作。但是,這個里的每一個異步操作都必須是 Thunk 函數,也就是說,跟在 yield 表達式后面的必須是 Thunk 函數。

3、Co 模塊

co 模塊是用于 Generator 函數自動執行的一個小工具。
co 模塊是講兩種自動執行器(Thunk 函數和 Promise 對象)包裝成一個模塊。使用 co 模塊的前提條件是,Generator 函數的 yield 名利后面只是 Thunk 函數或 Promise 對象。
使用 co 模塊處理并發的異步操作 :
1、并發操作放在數組里

co(function* () {
  var res = yield [
    Promise.resolve(1),
    Promise.resolve(2)
  ];
  console.log(res);
}).catch(onerror);

2、并發操作寫在對象里

co(function* () {
  var res = yield {
    1: Promise.resolve(1),
    2: Promise.resolve(2)
  };
  console.log(res);
}).catch(onerror);

3、處理多個 generator 異步操作

co(function* () {
  var valuse = [n1, n2, n3];
  yield values.map(somethingAsync);
});

function* somethingAsync(x) {
  // do something async
  return y;
}

上面代碼允許并發 3 個 somethingAsync 異步操作。

四、結語

本章需要了解的是生成器函數 Generator 的具體含義及作用,如何使用 Generator 函數,以及 Generator 函數的幾個特殊的應用實例。

戳我博客

章節目錄

1、ES6中啥是塊級作用域?運用在哪些地方?
2、ES6中使用解構賦值能帶給我們什么?
3、ES6字符串擴展增加了哪些?
4、ES6對正則做了哪些擴展?
5、ES6數值多了哪些擴展?
6、ES6函數擴展(箭頭函數)
7、ES6 數組給我們帶來哪些操作便利?
8、ES6 對象擴展
9、Symbol 數據類型在 ES6 中起什么作用?
10、Map 和 Set 兩數據結構在ES6的作用
11、ES6 中的Proxy 和 Reflect 到底是什么鬼?
12、從 Promise 開始踏入異步操作之旅
13、ES6 迭代器(Iterator)和 for...of循環使用方法
14、ES6 異步進階第二步:Generator 函數
15、JavaScript 異步操作進階第三步:async 函數
16、ES6 構造函數語法糖:class 類

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,362評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,013評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,346評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,421評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,146評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,534評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,585評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,767評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,318評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,074評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,258評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,828評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,486評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,916評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,156評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,993評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,234評論 2 375

推薦閱讀更多精彩內容

  • 異步編程對JavaScript語言太重要。Javascript語言的執行環境是“單線程”的,如果沒有異步編程,根本...
    呼呼哥閱讀 7,323評論 5 22
  • 在此處先列下本篇文章的主要內容 簡介 next方法的參數 for...of循環 Generator.prototy...
    醉生夢死閱讀 1,463評論 3 8
  • 簡介 基本概念 Generator函數是ES6提供的一種異步編程解決方案,語法行為與傳統函數完全不同。本章詳細介紹...
    呼呼哥閱讀 1,085評論 0 4
  • 筆記,總結摘錄自阮一峰筆記中有不少自己看書的總結 基本概念 核心目的:異步編程解決方案 關鍵概念:狀態機,執行權限...
    布蕾布蕾閱讀 5,109評論 0 4
  • 今天在群里看見一個引導頁的效果,感覺好不錯,于是就擼了下,先看看效果圖 我們可以看到,當我們滑動頁面的時候,會有一...
    ReturnYHH閱讀 2,084評論 0 3