Generator:JS執行權的真實操做者

前言

ES6提供了一種新型的異步編程解決方案:Generator函數(如下簡稱G函數)。它不是使用JS現有能力按照必定標準制定出來的東西(Promise是如此出生的),而是具備新型底層操做能力,與傳統編程徹底不一樣,表明一種新編程邏輯的高大存在。簡潔方便、受人喜好的async函數就是以它爲基礎實現的。編程

1 意義

JS引擎是單線程的,只有一個函數執行棧。
噹噹前函數執行完後,執行棧將其彈出,銷燬包含其局部變量的棧空間,並開始執行前一個函數。執行權由此單向穩定的在不一樣函數中切換。雖然Web Worker的出現使咱們可以自行建立多個線程,但這離靈活的控制:暫停執行、切換執行權和中間的數據交換等等,仍是頗有距離的。segmentfault

G函數的意義在於,它能夠在單線程的背景下,使執行權與數據自由的遊走於多個執行棧之間,實現協程式編程。
調用G函數後,引擎會爲其開闢一個獨立的函數執行棧(如下簡稱G棧)。在執行它的過程當中,能夠控制暫停執行,並將執行權轉出給主執行棧或另外一個G棧(棧在這裏可理解爲函數)。而此G棧不會被銷燬而是被凍結,當執行權再次回來時,會在與上次退出時徹底相同的條件下繼續執行。api

下面是一個簡單的交出和再次得到執行權的例子。數組

// 依次打印出:1 2 3 4 5。

let g = G();

console.log('1'); // 執行權在外部。
g.next(); // 開始執行G函數,遇到 yield 命令後中止執行返回執行權。
console.log('3'); // 執行權再次回到外部。
g.next(); // 再次進入到G函數中,從上次中止的地方開始執行,到最後自動返回執行權。
console.log('5');

function* G() {
  let n = 4;
  console.log('2');
  yield; // 遇到此命令,會暫停執行並返回執行權。
  console.log(n);
}

2 登堂

2.1 形式

G函數也是函數,因此具備普通函數該有的性質,不過形式上有兩點不一樣。一是在function關鍵字和函數名之間有一個*號,表示此爲G函數。二是隻有在G函數裏才能使用yield命令(以及yield*命令),處於其內部的非G函數也不行。因爲箭頭函數不能使用yield命令,所以不能用做於Generator函數(能夠用做於async函數)。瀏覽器

如下是它的幾種定義方式。服務器

// 聲明式
function* G() {}

// 表達式
let G = function* () {};

// 做爲對象屬性
let o = {
  G: function* () {}
};

// 做爲對象屬性的簡寫式
let o = {
  * G() {}
};

// 箭頭函數不能用做G函數,報錯!
let o = {
  G: *() => {}
};

// 箭頭函數能夠用做 async 函數。
let o = {
  G: async () => {}
};

2.2 執行

調用普通函數會直接執行函數體中的代碼,以後返回函數的返回值。但G函數不一樣,執行它會返回一個遍歷器對象(此對象與數組中的遍歷器對象相同),不會執行函數體內的代碼。只有當調用它的next方法(也多是其它實例方法)時,纔開始了真正執行。異步

在G函數的執行過程當中,碰到yieldreturn命令時會中止執行並將執行權返回。固然,執行到此函數末尾時天然會返回執行權。每次返回執行權以後再次調用它的next方法(也多是其它實例方法),會從新得到執行權,並從上次中止的地方繼續執行,直到下一個中止點或結束。async

// 示例一
let g = G();
g.next(); // 打印出 1
g.next(); // 打印出 2
g.next(); // 打印出 3

function* G() {
  console.log(1);
  yield;
  console.log(2);
  yield;
  console.log(3);
}

// 示例二
let gg = GG();

gg.next(); // 打印出 1
gg.next(); // 打印出 2
gg.next(); // 沒有打印

function* GG() {
  console.log(1);
  yield;
  console.log(2);
  return;
  yield;
  console.log(3);
}

3 入室

3.1 數據交互

數據若是不能在執行權的更替中取得交互,其存在的意義就會大打折扣。異步編程

G函數的數據輸出和輸入是經過yield命令和next方法實現的。
yieldreturn同樣,後面能夠跟上任意數據,程序執行到此會交出控制權並返回其後的跟隨值(沒有則爲undefined),做爲數據的輸出。每次調用next方法將控制權移交給G函數時,能夠傳入任意數據,該數據會等同替換G函數內部相應的yield xxx表達式,做爲數據的輸入。函數

執行G函數,返回的是一個遍歷器對象。每次調用它的next方法,會獲得一個具備valuedone字段的對象。value存儲了移出控制權時輸出的數據(即yieldreturn後的跟隨值),done爲布爾值表明該G函數是否已經完成執行。做爲遍歷器對象的它具備和數組遍歷器相同的其它性質。

// n1 的 value 爲 10,a 和 n2 的 value 爲 100。
let g = G(10);

let n1 = g.next(); // 獲得 n 值。
let n2 = g.next(100); // 至關將 yield n 替換成 100。

function* G(n) {
  let a = yield n; // let a = 100;
  console.log(a); // 100
  return a;
}

實際上,G函數是實現遍歷器接口最簡單的途徑,不過有兩點須要注意。一是G函數中的return語句,雖然經過遍歷器對象能夠得到return後面的返回值,但此時done屬性已爲true,經過for of循環是遍歷不到的。二是G函數能夠寫成爲永動機的形式,相似服務器監聽並執行請求,這時經過for of遍歷是沒有盡頭的。

--- 示例一:return 返回值。
let g1 = G();
console.log( g1.next() ); // value: 1, done: false
console.log( g1.next() ); // value: 2, done: true
console.log( g1.next() ); // value: undefined, done: true

let g2 = G();
for (let v of g2) {
  console.log(v); // 只打印出 1。
}

function* G() {
  yield 1;
  return 2;
}

--- 示例二:做爲遍歷器接口。
let o = {
  id: 1,
  name: 2,
  ago: 3,
  *[Symbol.iterator]() {
    let arr = Object.keys(this);
    for (let v of arr) {
      yield this[v]; // 使用 yield 輸出。
    }
  }
}

for (let v of o) {
  console.log(v); // 依次打印出:1 2 3。
}

--- 示例三:永動機。
let g = G();
g.next(); // 打印出: Do ... 。
g.next(); // 打印出: Do ... 。
// ... 能夠無窮次調用。

// 能夠嘗試此例子,雖然頁面會崩潰。
// 崩潰以後能夠點擊關閉頁面,或終止瀏覽器進程,或辱罵做者。
for (let v of G()) {
  console.log(v);
}

function* G() {
  while (true) {
    console.log('Do ...');
    yield;
  }
}

3.2 yield*

yield*命令的基本原理是自動遍歷並用yield命令輸出擁有遍歷器接口的對象,怪繞口的,直接看示例吧。

// G2 與 G22 函數等價。

for (let v of G1()) {
  console.log(v); // 打印出:1 [2, 3] 4。
}
for (let v of G2()) {
  console.log(v); // 打印出:1 2 3 4。
}
for (let v of G22()) {
  console.log(v); // 打印出:1 2 3 4。
}

function* G1() {
  yield 1;
  yield [2, 3];
  yield 4;
}

function* G2() {
  yield 1;
  yield* [2, 3]; // 使用 yield* 自動遍歷。
  yield 4;
}

function* G22() {
  yield 1;
  for (let v of [2, 3]) { // 等價於 yield* 命令。
    yield v;
  }
  yield 4;
}

在G函數中直接調用另外一個G函數,與在外部調用沒什麼區別,即使前面加上yield命令。但若是使用yield*命令就能直接整合子G函數到父函數中,十分方便。由於G函數返回的就是一個遍歷器對象,而yield*能夠自動展開持有遍歷器接口的對象,並用yield輸出。如此就等價於將子G函數的函數體原本來本的複製到父G函數中。

// G1 與 G2 等價。

for (let v of G1()) {
  console.log(v); // 依次打印出:1 2 '-' 3 4
}
for (let v of G2()) {
  console.log(v); // 依次打印出:1 2 '-' 3 4
}

function* G1() {
  yield 1;
  yield* GG();
  yield 4;
}

function* G2() {
  yield 1;
  yield 2;
  console.log('-');
  yield 3;
  yield 4;
}

function* GG() {
  yield 2;
  console.log('-');
  yield 3;
}

惟一須要注意的是子G函數中的return語句。yield*雖然與for of同樣不會遍歷到該值,但其能直接返回該值。

let g = G();

console.log( g.next().value ); // 1
console.log( g.next().value ); // undefined, 打印出 return 2。

function* G() {
  let n = yield* GG(); // 第二次執行 next 方法時,這裏等價於 let n = 2; 。
  console.log('return', n);
}

function* GG() {
  yield 1;
  return 2;
}

3.3 異步應用

歷經瞭如此多的鋪墊,是到將其應用到異步的時候了,來來來,喝了這壇酒咱就到馬路上碰個瓷試試運氣。
使用G函數處理異步的優點,相對於在這之前最優秀的Promise來講,在於形式上使主邏輯代碼更爲的精簡和清晰,使其看起來與同步代碼基本相同。雖然在平常生活中,咱們說誰誰作事愛搞形式多少包含有貶低意味。但在這程序的世界,對於咱們編寫和他人閱讀來講,這些改進的效益但是至關可觀哦。

// 模擬請求數據。
// 依次打印出 get api1, Do ..., get api2, Do ..., 最終值:3000 。

// 請求數據的主邏輯塊
function* G() {
  let api1 = yield createPromise(1000); // 發送第一個數據請求,返回的是該 Promise 。
  console.log('get api1', api1); // 獲得數據。
  console.log('Do somethings with api1'); // 作些操做。
  
  let api2 = yield createPromise(2000); // 發送第二個數據請求,返回的是該 Promise 。
  console.log('get api2', api2); // 獲得數據。
  console.log('Do somethings with api2'); // 作些操做。
  
  return api1 + api2;
}

// 開始執行G函數。
let g = G();
// 獲得第一個 Promise 並等待其返回數據
g.next().value.then(res => {
  // 獲取到第一個請求的數據。
  return g.next(res).value; // 將第一個數據傳回,並獲取到第二個 Promise 。
}).then(res => {
  // 獲取到第二個請求的數據。
  return g.next(res).value; // 將第二個數據傳回。
}).then(res => {
  console.log('最終值:', res);
});

// 模擬請求數據
function createPromise(time) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(time);
    }, time);
  });
}

上面的方式有很大的優化空間。咱們執行函數時的邏輯是:先獲取到異步請求並等待其返回結果,再將結果傳遞迴G函數,以後重複操做。而按照此方式,意味着G函數中有多少異步請求,咱們就應該重複多少次該操做。若是觀衆老爺們足夠敏感,此時就能想到這些步奏是能抽象成一個函數的。而抽象出來的這個函數就是G函數的自執行器。

如下是一個簡易的自執行器,它會返回一個Promise。再往內是經過遞歸一步步的執行G函數,對其返回的結果都統一使用resolve方法包裝成Promise對象。

// 與上一個示例等價。
RunG(G).then(res => {
  console.log('G函數執行結束:', res); // 3000
});

function* G() {
  let api1 = yield createPromise(1000);
  console.log('get api1', api1);
  console.log('Do somethings with api1');
  
  let api2 = yield createPromise(2000);
  console.log('get api2', api2);
  console.log('Do somethings with api2');
  
  return api1 + api2;
}

function RunG(G) {
  // 返回 Promise 對象。
  return new Promise((resolve, reject) => {
    let g = G();

    next();

    function next(data) {
      let r = g.next(data);

      // 成功執行完G函數,則改變 Promise 的狀態爲成功。
      if (r.done) return resolve(r.value);

      // 將每次的返回值統一包裝成 Promise 對象。
      // 成功則繼續執行G函數,不然改變 Promise 的狀態爲失敗。
      Promise.resolve(r.value).then(next).catch(reject);
    }
  });
}

function createPromise(time) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(time);
    }, time);
  });
}

自執行器能夠自動執行任意的G函數,是應用於異步時必要的咖啡伴侶。上面是接地氣的寫法,咱們來看看較爲官方的版本。能夠直觀的感覺到,二者主要的區別在對可能錯誤的捕獲和處理上,這也是日常寫的代碼和構建底層庫主要的區別之一。

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}

4 實例方法

實例方法好比next以及接下來的throwreturn,實際是存在G函數的原型對象中。執行G函數返回的遍歷器對象會繼承G函數的原型對象。在此添加自定義方法也能夠被繼承。這使得G函數看起來相似構造函數,但實際二者不相同。由於G函數本就不是構造函數,不能被new,內部的this也不能被繼承。

function* G() {
  this.id = 123;
}
G.prototype.sayName = () => {
  console.log('Wmaker');
};

let g = G();
g.id; // undefined
g.sayName(); // 'Wmaker'

4.1 throw

實例方法thrownext方法的性質基本相同,區別在於其是向G函數體內傳遞錯誤而不是值。通俗的表達是將yield xxx表達式替換成throw 傳入的參數。其它好比會接着執行到下一個斷點,返回一個對象等等,和next方法一致。該方法使得異常處理更爲簡單,並且多個yield表達式能夠只用一個try catch代碼塊捕獲。

當經過throw方法或G函數在執行中本身拋出錯誤時。若是此代碼正好被try catch塊包裹,便會像公園裏行完方便的寵物同樣,沒事的繼續往下執行。遇到下一個斷點,交出執行權傳出返回值。若是沒有錯誤捕獲,JS會終止執行並認爲函數已經結束運行,此後再調用next方法會一直返回valueundefineddonetrue的對象。

// 依次打印出:1, Error: 2, 3。
let g = G();

console.log( g.next().value ); // 1
console.log( g.throw(2).value ); // 3,打印出 Error: 2。

function* G() {
  try {
    yield 1;
  } catch(e) {
    console.log('Error:', e);
  }
  yield 3;
}

// 使用了 throw(2) 等價於使用 next() 並將代碼改寫成以下所示。

function* G() {
  try {
    yield 1;
    throw 2; // 替換原來的 yield 表達式,至關在後面添加。
  } catch(e) {
    console.log('Error:', e);
  }
  yield 3;
}

4.2 return

實例方法returnthrow的狀況相同,與next具備類似的性質。區別在於其會直接終止G函數的執行並返回傳入的參數。通俗的表達是將yield xxx表達式替換成return 傳入的參數。值得注意的是,若是此時正好處於try代碼塊中,且其帶有finally模塊,那麼return方法會推遲到finally代碼塊執行完後再執行。

let g = G();

console.log( g.next().value ); // 1
console.log( g.return(4).value ); // 2
console.log( g.next().value ); // 3
console.log( g.next().value ); // 4,G函數結束。
console.log( g.next().value ); // undefined

function* G() {
  try {
    yield 1;
  } finally {
    yield 2;
    yield 3;
  }
  yield 5;
}

// 使用了 return(4) 等價於使用 next() 並將代碼改寫成以下所示。

function* GG() {
  try {
    yield 1;
    return 4; // 替換原來的 yield 表達式,至關在後面添加。
  } finally {
    yield 2;
    yield 3;
  }
  
  yield 5;
}

延伸

ES6精華:Symbol
ES6精華:Promise
Iterator:訪問數據集合的統一接口

相關文章
相關標籤/搜索