面試: 怎麼往 Generator 裏拋個錯?

提示: 本文是 github 上《Understanding ECMAScript 6》 的筆記整理,代碼示例也來源於此。你們有時間能夠直接讀這本書。雖是英文,但通俗易懂,很是推薦。git

前情: 在上一篇文章 你知道爲何會有 Generator 嗎 裏,我拋磚引玉,介紹了 generator 產生的緣由。當時就有夥伴指出 「Generator是用來模擬多線程的休眠機制的」、 「Generator運行是惰性的」。那時我就說高級篇裏會有介紹,這裏就好好說一下。es6

摘要: 這裏的重點,首先是如何與generator裏通訊,一是用 next() 傳參,二是還能夠用 throw() ,不一樣的是它是往裏拋錯; 其次是有 yield 賦值語句時, generator 內部的執行順序; 最後會是怎麼用同步的方式寫異步(有可能像 co 哦)。github

原文地址redux

若是對 generator 不太熟的,能夠先看看 這裏promise

1. 傳參

簡單說就是能夠往next傳參數,而generatoryield 處能夠接收到這個參數, 以下例子:bash

function *createIterator() {
  let first = yield 1;
  let second = yield first + 2;    // 4 + 2
  yield second + 3;                // 5 + 3
}
let iterator = createIterator();   
console.log(iterator.next());      // "{ value: 1, done: false }"
console.log(iterator.next(4));     // "{ value: 6, done: false }"
console.log(iterator.next(5));     // "{ value: 8, done: false }"
console.log(iterator.next());      // "{ value: undefined, done: true }"
複製代碼

執行流程

要很明白地解釋上面的執行過程,可藉助這張圖:多線程

顏色相同的是同一次迭代裏執行的,由淺到深,表示迭代的前後順序。如:異步

  1. 第一次調用next(), 執行 yield 1 到中止,返回 { value: 1, done: false }注意,這時賦值語句 let fisrt = ... 沒有執行;
  2. 第二次調用 next(4), 先將參數 4 傳入上一次 yield 處,可理解爲:
let first = yield 1;

=>

let first = 4;
複製代碼

再從上次停頓的地方開始執行,就是說先執行賦值語句async

let first = 4
複製代碼

而後執行到下個yield爲止,即函數

yield first + 2  // 4 + 2
複製代碼

最後返回 { value: 6, done: false }

以後的 next 依上面的原理而執行,直到迭代完畢。

也就是說,經過next的參數,generator 產生的 iterator,與外部環境搭建起了溝通的橋樑,結合 iterator 能夠停頓的特色,能夠作一些有意思的事,如用同步方式寫回調等,詳見下文。

2. 往 iterator 裏拋錯

function *createIterator() {
  let first = yield 1;
  let second = yield first + 2; // yield 4 + 2, 而後拋出錯誤
  yield second + 3;             // 不會被執行
}
let iterator = createIterator();
	
console.log(iterator.next());  // {value: 1, done: false}
console.log(iterator.next(4)); // {value: 6, done: false}
console.log(iterator.throw(new Error("Boom"))); // generator 裏拋出的錯誤
複製代碼

根據上面說的執行機制,這裏例子的執行流程能夠用這張圖表示:

第三次執行迭代時,咱們調用 iterator.throw(new Error("Boom")), 向 iterator 裏拋出錯誤,傳入的參數爲錯誤信息。

咱們能夠改造 createIterator 以下:

function* createIterator() {
  let first = yield 1;
  let second;
  try {
    second = yield first + 2;
  } catch (ex) {
    second = 6;
  }
  yield second + 3;
}
let iterator = createIterator();                 
console.log(iterator.next());                   // "{ value: 1, done: false }"
console.log(iterator.next(4));                  // "{ value: 6, done: false }"
console.log(iterator.throw(new Error("Boom"))); // "{ value: 9, done: false }"
console.log(iterator.next());                   // "{ value: undefined, done: true }"
複製代碼

其執行流程解釋以下:

  1. 前兩次調用 next 狀況和上面執行機制裏的分析是同樣的,就不贅述了。

  2. 第三次調用 iterator.throw(new Error("Boom")generator 往拋入錯誤,函數內部在上次中止處即 yield first + 2 接收信息,拋出錯誤。可是被catch了,因此繼續執行到下一個停頓點:

    yield second + 3;  // 6 + 3
    複製代碼

    最後返回本次迭代結果 { value: 9, done: false }

  3. 繼續執行其餘迭代,和上沒無甚不一樣,不贅述。

小結: 這裏有能夠看到,next()throw() 均可以讓 iterator 繼續執行下去,不一樣的是後者會是以拋出錯誤的方式讓 iterator 繼續執行的。但在這以後,generator 裏會發生什麼,取決於代碼怎麼寫的了。

3. Generator 裏的 return 語句

這裏的 return 語句, 功能上與通常函數的 return 沒太大區別,都會阻止 return 以後的語句執行。

function* createIterator() {
  yield 1;
  return;
  yield 2;
  yield 3;
}
let iterator = createIterator();
console.log(iterator.next());  // "{ value: 1, done: false }"
console.log(iterator.next());  // "{ value: undefined, done: true }"
複製代碼

上面的 return, 使得以後的 yield 都被忽略了,因此,迭代二次而卒。

可是,若是 return 後有值,會被計入本次迭代的結果中:

function* createIterator() {
  yield 1;
  return 42;
}
let iterator = createIterator();
console.log(iterator.next());  // "{ value: 1, done: false }"
console.log(iterator.next());  // "{ value: 42, done: true }"
console.log(iterator.next());  // "{ value: undefined, done: true }"
複製代碼

這個iterator 執行兩次就可收攤了,和上一個例子不一樣的是,最後一次返回結果裏有 return 後的值 { value: 42, done: true }

又可是,這個返回值只能用一次,因此第三次執行next, 返回結果變成了 { value: undefined, done: true }

特別注意: 展開操做符...for-of 看到迭代結果裏 donetrue 就立刻中止執行,連 return 後面的值也無論了,中止得很決絕。如上面的例子,用for-of... 執行:

function* createIterator() {
  yield 1;
  return 42;
}

let iterator = createIterator();

for(let item of iterator) {
  console.log(item);
}
// 1

let anotherIterator = createIterator();
console.log([...anotherIterator]) 
// [1]

// 猜猜 [...iterator] 的結果是什麼
複製代碼

4. Generator 委託

generator 委託是什麼,簡單說就是把 generator A 委託給 generator B, 讓 B 代爲執行:

function* createNumberIterator() {
  yield 1;
  yield 2;
}
function* createColorIterator() {
  yield "red";
  yield "green";
}
function* createCombinedIterator() {
  yield* createNumberIterator();
  yield* createColorIterator();
  yield true;
}

var iterator = createCombinedIterator();

console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: "red", done: false }"
console.log(iterator.next()); // "{ value: "green", done: false }"
console.log(iterator.next()); // "{ value: true, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
複製代碼

以上可見,委託的語法,就是在一個 generator裏, 用 yield* 操做另外一個 generator 的執行結果。

經過委託把不一樣的 generator 放一塊兒,再利用return 的返回值,能夠在 generator 裏通訊,給出了更多的想象空間:

function* createNumberIterator() {
  yield 1;
  yield 2;
  return 3;
}
function* createRepeatingIterator(count) {
  for (let i = 0; i < count; i++) {
    yield "repeat";
  }
}
function* createCombinedIterator() {
  let result = yield* createNumberIterator();
  yield* createRepeatingIterator(result);
}
var iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
複製代碼

如上, createNumberIterator 的返回值 3 傳入了createRepeatingIterator 裏, 若是拆開寫,是這樣:

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

function* createRepeatingIterator(count) {
  for (let i = 0; i < count; i++) {
    yield "repeat";
  }
}

function* createCombinedIterator() {
  let result = yield* createNumberIterator();
  yield result;
  yield* createRepeatingIterator(result);
}

var iterator = createCombinedIterator();
console.log(iterator.next());  // "{ value: 1, done: false }"
console.log(iterator.next());  // "{ value: 2, done: false }"
console.log(iterator.next());  // "{ value: 3, done: false }"
console.log(iterator.next());  // "{ value: "repeat", done: false }"
console.log(iterator.next());  // "{ value: "repeat", done: false }"
console.log(iterator.next());  // "{ value: "repeat", done: false }"
console.log(iterator.next());  // "{ value: undefined, done: true }"
複製代碼

注意:既然 yield * 後面接的是 generator 的執行結果,而 generatoriterable。就是說,yield * 後能夠直接跟 iterable, 如字符串。如:

let g = function *() {
    yield *['a', 'b', 'c']
  }

  for(let item of g()) {
    console.log(item);
  }
  
  // a
  // b
  // c
複製代碼

5. Genarator 與異步

關於 js 裏異步的特色,這裏展開說了。簡單來說,它讓 js 這們單線程語言更強大; 可是,異步狀況一複雜好比有異步之間有依賴,那就很容易寫出以下的callback hell, 極難維護:

合理利用 genarator 就能夠用同步的寫法,寫異步。

從以前的介紹裏已經知道,genarator 返回 iterator, 須要手動調用 next, 很麻煩。那若是封裝一些,可讓 iterator 本身執行完畢,不就很好了:

  1. 前期準備,實現自動執行 generator 的函數

    run(function* () {
      let value = yield 1;
      console.log(value);
      value = yield value + 3;
      console.log(value);
    });
    複製代碼

    要讓它本身執行,那麼 run 須要:

    1. 執行 generator, 拿到 iterator;
    2. 調用 iterator.next();
    3. 把上一步的返回結果做爲下一次 iterator.next(lastResult) 參數,繼續迭代;
    4. 重複 3 ,直到迭代完畢。

    實現以下:

    function run(taskDef) {
      
      // 建立並保存 iterator,留到後面使用
      let task = taskDef();
      
      let result = task.next();
      
      // 遞歸地執行 `next`
      function step() {
        // 若是沒完的話
        if (!result.done) {
          result = task.next(result.value);
          step();
        }
      }
      // 開始處理
      step();
    }
    複製代碼
  2. 實現目標,用同步方式寫異步

    加入咱們要讓下面這段代碼可行:

    const asyncWork = new Promise((resolve, reject) => {
      setTimeout(() => resolve(5), 500)
    })
    
    
    run(function* () {
      let value = yield asyncWork;
      console.log(value)
      value = yield value + 3;
      console.log(value)
    });
    
    複製代碼

    這裏和上一個例子不一樣的地方在於,yield 返回結果多是個promise, 那咱們加個判斷就能夠了:

    if (result.value && typeof result.value.then === 'function') {
      result.value.then(d => {
        result = task.next(d)
        ... 
      })
    }
    複製代碼

    就是判斷若是是 promise, 執行 then 函數,把返回結果傳入下一次迭代 next(d) 便可。完整示例代碼以下:

    function run(taskDef) {
      
      // 建立並保存 iterator,留到後面使用
      let task = taskDef();
      
      let result = task.next();
      
      // 遞歸地執行 `next`
      function step() {
        
        // 若是沒完的話
        if (!result.done) {
          if (result.value && typeof result.value.then === 'function') {
            result.value.then(d => {
              result = task.next(d)
              step();
            })
          } else {
            result = task.next(result.value);
            step();
          }
        }
      }
      // 開始處理
      step();
    }
    複製代碼

    回頭看看這個寫法:

    run(function* () {
      let value = yield asyncWork;
      console.log(value)
      value = yield value + 3;
      console.log(value)
    });
    複製代碼

    雖然第二個 yield 對上一個 yield 結果有依賴,但不用寫成回調,看着跟同步同樣,很直白!

結語

generator 產生的 iterator, 能夠用next,在函數外部往 generator 裏傳數據, 又能夠經過 throw 往裏拋錯。它們至關於在 generator 裏對外打開了多個通訊窗口,這讓清晰的異步成爲可能。強大的 redux-saga 也是基於 generator 實現的。是否是有更多的玩法?一切都是拋磚引玉,不知道你們還有其餘玩法沒?

若是對 generator 由來不太清楚的,也能夠先看看 這裏

另外,這篇文章最早發佈在 github,是個關於 ES6 的系列文章。若是以爲能夠,幫忙 star 下唄,方便找工做啊。哎,找工做,真-是-累-啊!!!

相關文章
相關標籤/搜索