如何理解Generator

爲何要用generator

在前端開發過程當中咱們常常須要先請求後端的數據,再用拿來的數據進行使用網頁頁面渲染等操做,然而請求數據是一個異步操做,而咱們的頁面渲染又是同步操做,這裏ES6中的generator就能發揮它的做用,使用它能夠像寫同步代碼同樣寫異步代碼。下面是一個例子,先忽略下面的寫法,後面會詳細說明。若是你已經理解generator基礎能夠直接跳過這部分和語法部分,直接看深刻理解的部分。javascript

function *foo() {
  // 請求數據
  var data = yield makeAjax('http://www.example.com');
  render(data);
}
複製代碼

在等待數據的過程當中會繼續執行其餘部分的代碼,直到數據返回纔會繼續執行foo中後面的代碼,這是怎麼實現的那?咱們都知道js是單線程的,就是說咱們不可能同時執行兩段代碼,要實現這種效果,咱們先來猜測下(ps:固然後面我都看過了,這裏只是幫助你們理解,帶着問題去看),咱們來假設有一個「王杖」(指代cpu的執行權),誰拿到這個「王杖」,誰就能夠作本身想作的事,如今代碼執行到foo咱們如今拿着「王杖」而後向服務器請求數據,如今數據尚未返回,咱們不能幹等着。做爲王咱們有着高尚的馬克思主義思想,咱們先把本身的權利交出去,讓下一個須要用的人先用着,固然前提是要他們約定好一下子有須要,再把「王杖」還給咱們。等數據返回以後,咱們再把咱們的「王杖」要回來,就能夠繼續作咱們想作的事情了。 若是你理解了這個過程,那麼恭喜你,你已經基本理解了generator的運行機制,我這麼比喻雖然有些過程不是很貼切,但基本是這麼個思路。更多的東西仍是向下看吧。前端

generator語法

generator函數

在用generator以前,咱們首先要了解它的語法。在上面也看到過,它跟函數聲明很像,但後面有多了個*號,就是function *foo() { },固然也能夠這麼寫function* foo() { }。這裏兩種寫法沒有任何區別,全看我的習慣,這篇文章裏我會用第一種語法。如今咱們按這種語法聲明一個generator函數,供後面使用。java

function *foo() {

}
複製代碼

yield

到目前爲止,咱們還什麼也幹不了,由於咱們還缺乏了一個重要的老夥計yieldyield翻譯成漢語是產生的意思。yield會讓咱們跟在後面的表達式執行,而後交出本身的控制權,停在這裏,直到咱們調用next()纔會繼續向下執行。這裏新出現了next咱們先跳過,先說說generator怎麼執行。先看一個例子。git

function *foo() {
  var a = yield 1 + 1;
  var b = yield 2 + a;
  console.log(b);
}

var it = foo();
it.next();
it.next(2);
it.next(4);
複製代碼

下面咱們來逐步分析,首先咱們定義了一個generator函數foo,而後咱們執行它foo(),這裏跟普通函數不一樣的是,它執行完以後返回的是一個迭代器,等着咱們本身卻調用一個又一個的yield。怎麼調用那,這就用到咱們前面提到的next了,它可以讓迭代器一個一個的執行。好,如今咱們調用第一個it.next(),函數會從頭開始執行,而後執行到了第一個yield,它首先計算了1 + 1,嗯,而後停了下來。而後咱們調用第二個it.next(2),注意我這裏傳入了一個2做爲next函數的參數,這個2傳給了a做爲它的值,你可能還有不少其餘的疑問,咱們詳細的後面再說。接着來,咱們的it.next(2)執行到了第二個yield,並計算了2 + a因爲a2因此就變成了2 + 2。第三步咱們再調用it.next(4),過程跟上一步相同,咱們把b賦值爲4繼續向下執行,執行到了最後打印出咱們的b4。這就是generator執行的所有的過程了。如今弄明白了yieldnext的做用,回到剛纔的問題,你可能要問,爲何要在next中傳入24,這裏是爲了方便理解,我手動計算了1 + 12 + 2的值,那麼程序本身計算的值在哪裏?是next函數的返回值嗎,帶着這個疑問,咱們來看下面一部分。es6

next

next的參數

next能夠傳入一個參數,來做爲上一次yield的表達式的返回值,就像咱們上面說的it.next(2)會讓a等於2。固然第一次執行next也能夠傳入一個參數,但因爲它沒有上一次yield因此沒有任何東西可以接受它,會被忽略掉,因此沒有什麼意義。github

next的返回值

在這部分咱們說說next返回值,廢話很少說,咱們先打印出來,看看它究竟是什麼,你能夠本身執行一下,也能夠直接看我執行的結果。後端

function *foo() {
  var a = yield 1 + 1;
  var b = yield 2 + a;
  console.log(b);
}

var it = foo();
console.log(it.next());
console.log(it.next(2));
console.log(it.next(4));
複製代碼

執行結果:api

{ value: 2, done: false }
{ value: 4, done: false }
4
{ value: undefined, done: true }
複製代碼

看到這裏你會發現,yield後面的表達式執行的結果確實返回了,不過是在返回值的value字段中,那還有done字段使用來作什麼用的那。其實這裏的done是用來指示咱們的迭代器,就是例子中的it是否執行完了,仔細觀察你會發現最後一個it.next(4)返回值是done: true的,前面的都是false,那麼最後一個打印值的undefined又是什麼那,由於咱們後面沒有yield了,因此這裏沒有被計算出值,那麼怎麼讓最後一個有值那,很簡單加個return。咱們改寫下上面的例子。數組

function *foo() {
  var a = yield 1 + 1;
  var b = yield 2 + a;
  return b + 1;
}

var it = foo();
console.log(it.next());
console.log(it.next(2));
console.log(it.next(4));
複製代碼

執行結果:promise

{ value: 2, done: false }
{ value: 4, done: false }
{ value: 5, done: true }
複製代碼

最後的nextvalue的值就是最終return返回的值。到這裏咱們就再也不須要手動計算咱們的值了,咱們在改寫下咱們的例子。

function *foo() {
  var a = yield 1 + 1;
  var b = yield 2 + a;
  return b + 1;
}

var it = foo();
var value1 = it.next().value;
var value2 = it.next(value1).value;
console.log(it.next(value2));
複製代碼

大功告成!這些基本上就完成了generator的基礎部分。可是還有更多深刻的東西須要咱們進一步挖掘,看下去,相信你會有收穫的。

深刻理解

前兩部分咱們學習了爲何要用generator以及generator的語法,這些都是基礎,下面咱們來看點不同的東西,老規矩先帶着問題才能更有目的性的看,這裏先提出幾個問題:

  • 怎樣在異步代碼中使用,上面的例子都是同步的啊
  • 若是出現錯誤要怎麼進行錯誤的處理
  • 一個個調用next太麻煩了,能不能循環執行或者自動執行那

迭代器

進行下面全部的部分以前咱們先說一說迭代器,看到如今,咱們都知道generator函數執行完返回的是一個迭代器。在ES6中一樣提供了一種新的迭代方式for...offor...of能夠幫助咱們直接迭代出每一個的值,在數組中它像這樣。

for (var i of ['a', 'b', 'c']) {
  console.log(i);
}

// 輸出結果
// a
// b
// c
複製代碼

下面咱們用咱們的generator迭代器試試

function *foo() {
  yield 1;
  yield 2;
  yield 3;
  return 4;
}

// 獲取迭代器
var it = foo();

for(var i of it) {
  console.log(i);
}

// 輸出結果
// 1
// 2
// 3
複製代碼

如今咱們發現for...of會直接取出咱們每一次計算返回的值,直到done: true。這裏注意,咱們的4沒有打印出來,說明for...of迭代,是不包括donetrue的時候的值的。

下面咱們提一個新的問題,若是在generator中執行generator會怎麼樣?這裏咱們先認識一個新的語法yield *,這個語法可讓咱們在yield跟一個generator執行器,當yield遇到一個新的generator須要執行,它會先將這個新的generator執行完,再繼續執行咱們當前的generator。這樣說可能不太好理解,咱們看代碼。

function *foo() {
  yield 2;
  yield 3;
  yield 4;
}

function * bar() {
  yield 1;
  yield *foo();
  yield 5;
}

for ( var v of bar()) {
  console.log(v);
}
複製代碼

這裏有兩個generator咱們在bar中執行了foo,咱們使用了yield *來執行foo,這裏的執行順序會是yield 1,而後遇到foo進入foo中,繼續執行foo中的yield 2直到foo執行完畢。而後繼續回到bar中執行yield 5因此最後的執行結果是:

1
2
3
4
5
複製代碼

異步請求

咱們上面的例子一直都是同步的,但實際上咱們的應用是在異步中,咱們如今來看看異步中怎麼應用。

function request(url) {
  makeAjaxCall(url, function(response) {
    it.next(response);
  })
}

function *foo() {
  var data = yield request('http://api.example.com');
  console.log(JSON.parse(data));
}

var it = foo();
it.next();
複製代碼

這裏又回到一開頭說的那個例子,異步請求在執行到yield的時候交出控制權,而後等數據回調成功後在回調中交回控制權。因此像同步同樣寫異步代碼並非說真的變同步了,只是異步回調的過程被封裝了,從外面看不到而已。

錯誤處理

咱們都知道在js中咱們使用try...catch來處理錯誤,在generator中相似,若是在generator內發生錯誤,若是內部能處理,就在內部處理,不能處理就繼續向外冒泡,直到可以處理錯誤或最後一層。

內部處理錯誤:

// 內部處理
function *foo() {
  try {
    yield Number(4).toUpperCase();
  } catch(e) {
    console.log('error in');
  }
}

var it = foo();
it.next();

// 運行結果:error in
複製代碼

外部處理錯誤:

// 外部處理
function *foo() {
  yield Number(4).toUpperCase();
}

var it = foo();
try {
  it.next();
} catch(e) {
  console.log('error out');
}

// 運行結果:error out
複製代碼

generator的錯誤處理中還有一個特殊的地方,它的迭代器有一個throw方法,可以將錯誤丟回generator中,在它暫停的地方報錯,再日後就跟上面同樣了,若是內部能處理則內部處理,不能內部處理則繼續冒泡。

內部處理結果:

function *foo() {
  try {
    yield 1;
  } catch(e) {
    console.log('error', e);
  }
  yield 2;
  yield 3;
}

var it = foo();
it.next();
it.throw('oh no!');

// 運行結果:error oh no!
複製代碼

外部處理結果:

function *foo() {
  yield 1;
  yield 2;
  yield 3;
}

var it = foo();
it.next();
try {
  it.throw('oh no!');
} catch (e) {
  console.log('error', e);
}

// 運行結果:error oh no!
複製代碼

根據測試,發現迭代器的throw也算做一次迭代,測試代碼以下:

function *foo() {
  try {
    yield 1;
    yield 2;
  } catch (e) {
    console.log('error', e);
  }
  yield 3;
}

var it = foo();
console.log(it.next());
it.throw('oh no!');
console.log(it.next());

// 運行結果
// { value: 1, done: false }
// error oh no!
// { value: undefined, done: true }
複製代碼

當用throw丟回錯誤的時候,除了try中的語句,迭代器迭代掉了yield 3下次再迭代就是,就是最後結束的值了。錯誤處理到這裏就沒有了,就這麼點東西^_^。

自動運行

generator能不能自動運行?固然能,而且有不少這樣的庫,這裏咱們先本身實現一個簡單的。

function run(g) {
  var it = g();

  // 利用遞歸進行迭代
  (function iterator(val) {
    var ret = it.next(val);

    // 若是沒有結束
    if(!ret.done) {
      // 判斷promise
      if(typeof ret.value === 'object' && 'then' in ret.value) {
        ret.value.then(iterator);
      } else {
        iterator(ret.value);
      }
    }
  })();
}
複製代碼

這樣咱們就能自動處理運行咱們的generator了,固然咱們這個很簡單,沒有任何錯誤處理,如何讓多個generator同時運行,這其中涉及到如何進行控制權的轉換問題。我寫了一個簡單的執行器Fo,其中包含了Kyle Simpson大神的一個ping-pong的例子,感興趣的能夠看下這裏是傳送門,固然能順手star一下就更好了,see you next article ~O(∩_∩)O~。

參考連接

相關文章
相關標籤/搜索