你懂 JavaScript 嗎?#25 產生器(Generator)

你所不知道的 JS

本文主要會談到

Generator

當函式執行開始後,它就會一直執行直到完成為止,沒有什麼人是可以打斷它的

如下,foo 會印出從 1 到 1000 的數字,接著的是原本預定 1ms 後要印出的字串「終於輪到我了」。由於必須等候 foo 執行完畢才能接續後面的工作,因此 setTimeout 的 callback 可能不只等了 1ms 而已。

function foo() {
  for (let i = 1; i <= 1000; i++) {
    console.log(`再等一下,i = ${i}`);
  }
}

setTimeout(() => {
  console.log('終於輪到我了');
}, 1);

foo();

執行結果

再等一下,i = 1
再等一下,i = 2
再等一下,i = 3

...

...

...

再等一下,i = 1000
終於輪到我了

基本上就跟薩諾斯一樣,只要戴上無限手套,沒人可以阻止他毀滅世界的。

薩諾斯

圖片來源

But!最重要的就是這個 But!ES6 出了一個新功能「產生器(generator)」,讓我們可以讓函式從內部暫停,再從外部恢復執行,意思就是我們找到了一個開關,可以來控制薩諾斯了!當他戴上無限手套,我們就立刻將時間暫停(順便幫他脫下手套?),然後再恢復運作(咦?手套不見惹!森 77!)。

我們來看看這個能讓時間暫停的神奇工具!

若希望將 foo 標記為 generator,我們必須在 function 與 foo 中間加上一個星號(*),加在 function 之後(function* foo() { ... })或 foo 之前(function *foo() { ... })都是可以的。

再來,當程式開始執行後,執行到 yield 就會停下來,並將其右手邊的運算式當回傳值包成物件丟出去,例如:第一輪的迴圈中,執行到 yield 停下來,把右手邊的 再等一下,i = 1 包在物件的 value 中當結果回傳出去,並且由於程式尚未執行完畢,done 為 false,因此得到 {value: "再等一下,i = 1", done: false}

然後,使用 next 重新啟動…我們就不停的這麼做…暫停 -> 啟動 -> 暫停 …,直到迭代停止。

function* foo() {
  for (let i = 1; i <= 3; i++) {
    let x = yield `再等一下,i = ${i}`;
    console.log(x);
  }
}

setTimeout(() => {
  console.log('終於輪到我了');
}, 1);

var a = foo();
console.log(a); // foo {<closed>}

var b = a.next();
console.log(b); // {value: "再等一下,i = 1", done: false}

var c = a.next();
console.log(c); // {value: "再等一下,i = 2", done: false}

var d = a.next();
console.log(d); // {value: "再等一下,i = 3", done: false}

var e = a.next();
console.log(e); // {value: undefined, done: true}

// 終於輪到我了

這邊要注意一下,上例中的 console.log(x) 會得到 undefined,那修改成下面這樣呢?

function* foo() {
  for (let i = 1; i <= 3; i++) {
    let x = yield `再等一下,i = ${i}`;
    console.log(x);
  }
}

setTimeout(() => {
  console.log('終於輪到我了');
}, 1);

var a = foo();
console.log(a); // foo {<closed>}

var b = a.next(1);
console.log(b); // {value: "再等一下,i = 1", done: false}

var c = a.next(2);
console.log(c); // {value: "再等一下,i = 2", done: false}

var d = a.next(3);
console.log(d); // {value: "再等一下,i = 3", done: false}

var e = a.next(4);
console.log(e); // {value: undefined, done: true}

// 終於輪到我了

第一次的 next 傳入值會被忽略,這是由這時候還沒有正在等候的運算式需要任何輸入值來完成工作的緣故,接下來的 next 傳入的值會成為 yield 的輸入值,因此 let x = 2,然後就會印出 2 了,依此類推。

generator 真的是很神奇的東西!繼續來看怎麼優化我們的程式碼。

Callback

在過去沒有 promise 的恐龍時代裡,若想解決非同步的等候問題,就必須包裹層層 callback…(點此複習 callback

上次看過兩數相加了,這次來看三數相加好了。

getA(function (a) {
  getB(function (b) {
    getC(function (c) {
      console.log(a + b + c); // 6
    });
  });
});

function getA(cb) {
  // 經過冗長運算或從伺服器取得...
  cb(1);
}

function getB(cb) {
  // 經過冗長運算或從伺服器取得...
  cb(2);
}

function getC(cb) {
  // 經過冗長運算或從伺服器取得...
  cb(3);
}

Promise

有 promise 之後,改寫上面的例子…(點此複習 promise)

function getA() {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(1);
    }, 1000);
  });
}

function getB() {
  return new Promise(function (resolve, reject) {
    resolve(2);
  });
}

function getC() {
  return new Promise(function (resolve, reject) {
    resolve(3);
  });
}

function add(xPromise, yPromise, zPromise) {
  return Promise.all([xPromise, yPromise, zPromise]).then(function (values) {
    return values[0] + values[1] + values[2];
  });
}

add(getA(), getB(), getC()).then(function (sum) {
  console.log(sum); // 6
});

Promise + Generator

再加上 generator 呢…

function getA() {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(1);
    }, 1000);
  });
}

function getB() {
  return new Promise(function (resolve, reject) {
    resolve(2);
  });
}

function getC() {
  return new Promise(function (resolve, reject) {
    resolve(3);
  });
}

function* add() {
  var x = yield getA();
  var y = yield getB();
  var z = yield getC();

  console.log(x + y + z); // 6
}

var gen = add();
gen.next().value.then(function (r1) {
  gen.next(r1).value.then(function (r2) {
    gen.next(r2).value.then(function (r3) {
      gen.next(r3);
    });
  });
});

Aysnc 與 Await

Aysnc 與 Await 的相關說明有很多,在此就不贅述了,有興趣的可以看卡斯伯的這篇文章。

改寫上例,雖然是非同步但根本寫得像是同步一樣…

function getA() {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(1);
    }, 1000);
  });
}

function getB() {
  return new Promise(function (resolve, reject) {
    resolve(2);
  });
}

function getC() {
  return new Promise(function (resolve, reject) {
    resolve(3);
  });
}

async function add() {
  var x = await getA();
  var y = await getB();
  var z = await getC();

  console.log(x + y + z);
}

add(); // 6

回顧

看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到…

References


同步發表於2019 鐵人賽


generator 產生器 async await ES6 You-Dont-Know-JS javascript 2019鐵人賽 你所不知道的JS 你懂JavaScript嗎? 鐵人賽 You-Dont-Know-JS-Async-and-Performance 你懂 JavaScript 嗎?2019 iT 邦幫忙鐵人賽 系列文