初探ES6特性-Generator

Javascript function的特性

Anderson
7 min readMay 7, 2017

Javascript函式的特性就是一旦開始執行,就無法被停止。那如果我想在中間停下來,先去執行其他的支線任務,等做完再回來做主線任務,這樣我不就辦不到了嗎 ? 如下面的範例,雖然我有設一毫秒後才執行印出log的function,但下面的function仍然必須等執行完畢後才將主控權交還給setTimeout。(這故事告訴我們,VIP也是要乖乖排隊…)

setTimeout(function () {
console.log("我是VIP,讓我插一下隊啦", (new Date).getMilliseconds());
}, 1);
function f() {
for (var i = 1; i <= 100; i++) {
console.log(`第${i}位進場`, (new Date).getMilliseconds());
}
}
f();

執行結果如下

第1位進場 460
第2位進場 462
第3位進場 463

第98位進場 465
第99位進場 465
第100位進場 465
我是VIP,讓我插一下隊啦 466

ES6 Generator

ES6 Generator容許執行到一半時,可以暫停多次,直到完成後再繼續接下來的工作。可以把JS寫法看起來像是同步的寫法。首先,我們先介紹一下Generator的基本語法,

  • Generator function:可以包含多個yield來達到暫停和繼續的目的。如:function* f(…) {…}。和一般的function不同的是function旁會多一個星號。
  • Generator object:如var it = f(),it得到並不是f()回傳的結果,而是得到一個generator object,當然他也繼承iterator的特性,可以執行類似loop等操作。iterator物件實行了iterator介面,所以有iterator的特性,回傳的format為{ value: Any, done: Boolean }。
  • yield:function中包含yield時,程式執行中遇到yield時,可以暫時把主控權給交出去,以達到類似同步的效果。當然一個function中可以包含多個yield來達到暫停和繼續的目的。yield很類似return的效果,但特別的下一次進來時會從上次中斷的地方繼續執行。
  • next:在function外,可以用it.next()來控制function的執行順序,當我們呼叫it.next()時,會回傳一個帶有value和done的物件。value代表function回傳的值,而done若為false,代表iterator尚未跑到最後一筆。在next中也可加入變數,如it.next(v),v就是下一次要傳回給generator的參數。

讓我們來看個例子,來驗證我們上述所提到genarator的特性,

function* f(n) {
yield n;
yield "Anderson";
yield n + 1;
return n + 2;
}
var it = f(1);
console.log(it.next().value);
// 1
console.log(it.next().value);
// Anderson
console.log(it.next());
// { value: 2, done: false }
console.log(it.next());
// { value: 3, done: true }
for (var v of f(100)) { console.log(v); }
// 100
// Anderson
// 101

特別的是最後一個迴圈並不會印出結果102,因為generator遇到return就結束迴圈,所以也不會回傳iterator的結果。

再來看下一個建立帳號的例子,

function* createAccount(n) {
var x = yield n;
console.log("帳號:", x);
var y = yield n + 1;
console.log("密碼:", y);
}
var step = 1;
var it = createAccount(step);
console.log(it.next("init").value);
console.log(it.next("Anderson").value);
console.log(it.next("12345"));
// Result:
// 1 (目前還沒有yield,所以init的值仍無法指派)
// 帳號: Anderson (Anderson指派給x)
// 2
// 密碼: 12345 (12345指派給y)
// { value: undefined, done: true }

generator function也可以在function中另一個generator function,範例如下,

function* sendWelcomMsg() {
yield 'Welcome home';
}
function* getUserInfo() {
yield 'account: Anderson';
yield 'phone: 123-4567';
yield *sendWelcomMsg();
}
for (var v of getUserInfo()) {
console.log(v);
}
// Result:
// account: Anderson
// phone: 123-4567
// Welcome home

再來看一個模擬使用Ajax去呼叫API,會回傳url和所帶入的參數的非同步應用,如果用一般的function來實作的結果如下,

function callApi(url, cb) {
setTimeout(() => {
var obj = { url: url, params: url.split('/')[1] };
cb(JSON.stringify(obj));
}, 1000);
}
callApi("user/1", v => {
console.log(v);
callApi("friends/1", r => {
console.log(r);
});
});
// Result:
// {"url":"user/1","params":"1"} (隔一秒後)
// {"url":"friends/1","params":"1"} (隔一秒後)

如果我們頻繁呼叫API,且每次都要從API拿回資料做下一次的參數,上面就會寫出如callback 地獄的寫法了,若改為用generator實做呢?

function callApi(url, cb) {
setTimeout(() => {
var obj = { url: url, params: url.split('/')[1]};
cb(JSON.stringify(obj));
}, 1000);
}
function request(url){
callApi(url, res => {
it.next(res);
})
}
function* main() {
var result = yield request("user/1");
var data = JSON.parse(result);
console.log(data);
result = yield request("friends/1");
data = JSON.parse(result);
console.log(data);
}
var it = main();
it.next();
// Result:
// { url: 'user/1', params: '1' }
// { url: 'friends/1', params: '1' }

在main這個Generator function寫出看起來像是同步(synchronous)的程式碼了,雖然額外多拉出一個function,但是我們把callApi和request拆成只負責某件事的function。優點如下,

  1. 不會寫出像callback 內縮地獄的寫法。
  2. 回傳的結果也不用等到promise.then才能取得。
  3. 讓程式看起來像是同步的寫法。

--

--

Anderson

A front-end developer, like the pursuit of beautiful things in life. Hope you like my blog.