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

jeyvie發表於2018-04-27

提示: 本文是 github 上《Understanding ECMAScript 6》 的筆記整理,程式碼示例也來源於此。大家有時間可以直接讀這本書。雖是英文,但通俗易懂,非常推薦。

前情: 在上一篇文章 你知道為什麼會有 Generator 嗎 裡,我拋磚引玉,介紹了 generator 產生的原因。當時就有夥伴指出 “Generator是用來模擬多執行緒的休眠機制的”、 “Generator執行是惰性的”。那時我就說高階篇裡會有介紹,這裡就好好說一下。

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

原文地址

如果對 generator 不太熟的,可以先看看 這裡

1. 傳參

簡單說就是可以往next傳引數,而generatoryield 處可以接收到這個引數, 如下例子:

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 }"
複製程式碼

執行流程

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

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

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

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

=>

let first = 4;
複製程式碼

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

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 裡丟擲的錯誤
複製程式碼

根據上面說的執行機制,這裡例子的執行流程可以用這張圖表示:

面試: 怎麼往 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, 極難維護:

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

合理利用 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 下唄,方便找工作啊。哎,找工作,真-是-累-啊!!!

相關文章