JavaScript Generators指南

圖片描述
最近,爲了更好地理解Redux Sagas的工做原理,我重學了JavaScript generators的知識,我把從網上收集到的各類知識點濃縮到一篇文章裏,我但願這篇文章既通俗易懂,又足夠嚴謹,能夠做爲初學者的generators使用指南。前端

簡介

JavaScript在ES6時引入了生成器。生成器函數與常規函數相似,除了能夠暫停和恢復它們這一點之外。生成器也與迭代器密切相關,由於生成器對象就是迭代器。
在JavaScript中,函數調用後一般不能暫停或中止。(是的,異步函數在等待await語句時暫停,可是異步函數在ES7時才引入。此外,異步函數是創建在生成器之上的。)一個普通函數只有在返回或拋出錯誤時纔會結束。算法

function foo() {
  console.log('Starting');
  const x = 42;
  console.log(x);  
  console.log('Stop me if you can');  
  console.log('But you cannot');
 }

相反,生成器容許咱們在任意斷點處暫停執行,並從同一斷點恢復執行。編程

生成器和迭代器

來自MDN數組

在JavaScript中,迭代器是一個對象,它定義一個序列,並在終止時可能返回一個返回值。 >更具體地說,迭代器是經過使用 next() 方法實現 Iterator protocol >的任何一個對象,該方法返回具備兩個屬性的對象: value,這是序列中的 next 值;和 done, 若是已經迭代到序列中的最後一個值,則它爲 true 。若是 value 和 done 一塊兒存在,則它是迭代器的返回值。

所以,迭代器的本質就是:promise

  • 定義序列的對象
  • 有一個next()方法…
  • 返回一個具備兩個屬性的對象:value和done

是否須要生成器來建立迭代器?不。事實上,咱們已經可使用閉包pre-ES6建立一個無限的斐波那契數列,以下例所示:閉包

var fibonacci = {
  next: (function () {
    var pre = 0, cur = 1;
    return function () {
      tmp = pre;
      pre = cur;
      cur += tmp;
      return cur;
    };
  })()
};

fibonacci.next(); // 1
fibonacci.next(); // 2
fibonacci.next(); // 3
fibonacci.next(); // 5
fibonacci.next(); // 8

關於生成器的好處,我將再次引用MDN異步

雖然自定義迭代器是一個有用的工具,可是因爲須要顯式地維護它們的內部狀態,建立它們須要咱們仔細地編程。生成器函數提供了一個強大的替代方法:它們容許咱們經過編寫一個執行不是連續的函數來定義迭代算法。
換句話說,使用生成器建立迭代器更簡單(不須要閉包!),這意味着出錯的可能性更小。
生成器和迭代器之間的關係就是生成器函數返回的生成器對象是迭代器。

語法

生成器函數使用function *語法建立,並使用yield關鍵字暫停。
最初調用生成器函數並不執行它的任何代碼;相反,它返回一個生成器對象。該值經過調用生成器的next()方法來使用,該方法執行代碼,直到遇到yield關鍵字,而後暫停,直到再次調用next()。ide

function * makeGen() {
  yield 'Hello';
  yield 'World';
}

const g = makeGen(); // g is a generator
g.next(); // { value: 'Hello', done: false }
g.next(); // { value: 'World', done: false }
g.next(); // { value: undefined, done: true }

在上面的最後一個語句以後重複調用g.next()只會返回(或者更準確地說,產生)相同的返回對象:{ value: undefined, done: true }。函數

yield暫停執行

你們可能會注意到上面的代碼片斷有一些特殊之處。第二個next()調用生成一個對象,該對象的屬性爲done: false,而不是done: true。
既然咱們正在生成器函數中執行最後一條語句,那麼done屬性不該該爲true嗎?並非的。當遇到yield語句時,它後面的值(在本例中是「World」)被生成,執行暫停。所以,第二個next()調用暫停在第二個yield語句上,所以執行尚未完成—只有在第二個yield語句以後執行從新開始時,執行纔算完成(即done: true),而且再也不運行代碼。
咱們能夠將next()調用看做是告訴程序運行到下一個yield語句(假設它存在)、生成一個值並暫停。程序在恢復執行以前不會知道yield語句以後沒有任何內容,而且只能經過另外一個next()調用恢復執行。工具

yield和return

在上面的示例中,咱們使用yield將值傳遞給生成器外部。咱們也可使用return(就像在普通函數中同樣);可是,使用return能夠終止執行並設置done: true。

function * makeGen() {
  yield 'Hello';
  return 'Bye';
  yield 'World';
}

const g = makeGen(); // g is a generator
g.next(); // { value: 'Hello', done: false }
g.next(); // { value: 'Bye', done: true }
g.next(); // { value: undefined, done: true }

由於執行不會在return語句上暫停,並且根據定義,在return語句以後不能執行任何代碼,因此done被設置爲true。

yield:next方法的參數

到目前爲止,咱們一直在使用yield傳遞生成器外部的值(並暫停其執行)。
然而,yield其實是雙向的,而且容許咱們將值傳遞到生成器函數中。

function * makeGen() {
  const foo = yield 'Hello world';
  console.log(foo);
}

const g = makeGen();
g.next(1); // { value: 'Hello world', done: false }
g.next(2); // logs 2, yields { value: undefined, done: true }

等一下。不該該是"1"打印到控制檯,可是控制檯打印的是"2"?起初,我發現這部分在概念上與直覺相反,由於我預期的賦值foo = 1。畢竟,咱們將「1」傳遞到next()方法調用中,從而生成Hello world,對嗎?
但事實並不是如此。傳遞給第一個next(...)調用的值將被丟棄。除了這彷佛是ES6規範以外,實際上沒有其餘緣由.從語義上講,第一個next方法用來啓動遍歷器對象,因此不用帶有參數。
我喜歡這樣對程序的執行進行合理化:

  • 在第一個next()調用時,它將一直運行,直到遇到yield 'Hello world',在此基礎上生成{ value: 'Hello world', done: false }和暫停。就是這麼回事。正如你們所看到的,傳遞給第一個next()調用的任何值都是不會被使用的(所以被丟棄)。
  • 當再次調用next(...)時,執行將恢復。在這種狀況下,執行須要爲常量foo分配一些值(由yield語句決定)。所以,咱們對next(2)的第二次調用賦值foo=2。程序不會在這裏中止—它會一直運行,直到遇到下一個yield或return語句。在本例中,沒有更多的yield,所以它記錄2並返回undefined的done: true。在生成器使用異步由於yield是一個雙向通道,容許信息在兩個方向上流動,因此它容許咱們以很是酷的方式使用生成器。到目前爲止,咱們主要使用yield在生成器以外傳遞值。可是咱們也能夠利用yield的雙向特性以同步方式編寫異步函數。

使用上面的概念,咱們能夠建立一個相似於同步代碼但實際上執行異步函數的基本函數:

function request(url) {
  fetch(url).then(res => {
    it.next(res); // Resume iterator execution
  });
}

function * main() {
  const rawResponse = yield request('https://some-url.com');
  const returnValue = synchronouslyProcess(rawResponse);
  console.log(returnValue);
}

const it = main();
it.next(); // Remember, the first next() call doesn't accept input

這是它的工做原理。首先,咱們聲明一個request函數和main生成器函數。接下來,經過調用main()建立一個迭代器it。而後,咱們從調用it.next()開始。
在第一行的function * main(),在yield request('https://some-url.com')以後執行暫停。request()隱式地返回undefined,所以咱們實際上生成了undefined值,但這並不重要—咱們沒有使用該值。
當request()函數中的fetch()調用完成時,it.next(res)將會被調用並完成下列兩件事:
it繼續執行;和
it將res傳遞給生成器函數,該函數被分配給rawResponse
最後,main()的其他部分將同步完成。
這是一個很是基礎的設置,應該與promise有一些類似之處。有關yield和異步性的更詳細介紹,請參閱此文。

生成器是一次性

咱們不能重複使用生成器,但能夠從生成器函數建立新的生成器。

function * makeGen() {
  yield 42;
}

const g1 = makeGen();
const g2 = makeGen();
g1.next(); // { value: 42, done: false }
g1.next(); // { value: undefined, done: true }
g1.next(); // No way to reset this!
g2.next(); // { value: 42, done: false }
...
const g3 = makeGen(); // Create a new generator
g3.next(); // { value: 42, done: false }

無限序列

迭代器表示序列,有點像數組。因此,咱們應該可以將全部迭代器表示爲數組,對吧?
然而,並非的。數組在建立時須要當即分配,而迭代器是延遲使用的。數組是迫切須要的,由於建立一個包含n個元素的數組須要首先建立/計算全部n個元素,以便將它們存儲在數組中。相反,迭代器是惰性的,由於序列中的下一個值只有在使用時纔會建立/計算。
所以,表示無限序列的數組在物理上是不可能的(咱們須要無限內存來存儲無限項!),而迭代器能夠輕鬆地表示(而不是存儲)該序列。
讓咱們建立一個從1到正無窮數的無窮序列。與數組不一樣,這並不須要無限內存,由於序列中的每一個值只有在使用時纔會懶散地計算出來。

function * makeInfiniteSequence() {
  var curr = 1;
  while (true) {
    yield curr;
    curr += 1;
  }
}

const is = makeInfiniteSequence();
is.next(); { value: 1, done: false }
is.next(); { value: 2, done: false }
is.next(); { value: 3, done: false }
... // It will never end

有趣的事實:這相似於Python生成器表達式vs列表理解。雖然這兩個表達式在功能上是相同的,可是生成器表達式提供了內存優點,由於值的計算是延遲的,而列表理解則是當即計算值並建立整個列表。

看以後

點贊,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓-_-)
關注公衆號「新前端社區」,享受文章首發體驗!
每週重點攻克一個前端技術難點。

相關文章
相關標籤/搜索