無論是 Node.js 或者是瀏覽器中,JavaScript 的運行流程,多半會是非同步方式,就目前來說,可以使用 setTimeout
來簡單地模擬非同步,例如:
function asyncFoo(n, callback) {
setTimeout(() => {
callback(n * Math.random());
}, 2000);
}
asyncFoo(10, r => console.log(r));
執行完 setTimeout
後,函式立即返回,而在時間到時,指定的回呼函式才會執行,在事情簡單時,這種方式沒什麼問題,然而,若希望事件發生時後,依序執行下一次非同步時,就會引發回呼地獄的問題:
asyncFoo(10, r1 => {
asyncFoo(r1, r2 => {
asyncFoo(r2, r3 => {
console.log(r3);
});
});
});
就算使用了 ES6 的箭號函式,在這種非同步模式下,可讀性也是迅速降低,如果有個 asyncFoo
可以傳回 Promise
,那事情會好辦的多:
function asyncFoo(n) {
return new Promise(resolve => {
setTimeout(() => {
resolve(n * Math.random()); // 完成約定
}, 2000);
});
}
asyncFoo(10)
.then(r1 => asyncFoo(r1))
.then(r2 => asyncFoo(r2))
.then(r3 => console.log(r3));
Promise
是 ES6 新增的 API,在建立 Promise
實例時,可以傳入一個回呼函式,該函式具有兩個參數,可命名為 resolve
與 reject
,這兩個參數會各自接受函式,若呼叫 resolve
,表示此次 Promise
的任務完成,若 Promise
實例曾使用 then
組合下一次非同步操作,那麼會呼叫指定的下個函式,then
也會傳回一個 Promise
,因此,雖說是非同步,然而撰寫風格上,就會像是循序的。
如果指定的任務無法達成,約定就無法滿足(fulfilled),此時可以由傳入的 reject
函式來背棄(reject)約定,例如:
new Promise((resolve, reject) => {
let n = Math.floor(Math.random() * 10);
if(n !== 0) {
resolve(n);
} else {
reject('zero');
}
}).then(
n => console.log(n),
shit => console.log('shit happens', shit)
);
Promise
的 then
方法可以接受兩個函式,一個是在滿足約定時執行,另一個是在背棄約定被時執行,上面的例子在隨機數為 0 時會背棄約定。
如果約定在執行任務時發生例外,會隱含地背棄約定,當約定被背棄時,可以在 then
的第二個參數指定函式來處理,也可以使用 catch
指定函式來處理:
function dividedRandom(n) {
return new Promise(resolve => {
let r = n / Math.floor(Math.random() * 10);
if(Number.isFinite(r)) {
resolve(r);
} else {
throw 'divided by zero';
}
});
}
dividedRandom(10)
.then(n => console.log(n))
.catch(err => console.log(err));
當你只關心約定是否被背棄時,可以只撰寫 catch
,從而避免了必須在 then
的第一個參數指定 nope 函式的情況:
dividedRandom(10)
.then(
() => {}, // 捨事都不做
err => console.log(err)
);
你可以有多個 then
來組合多個操作,然後接上一個 catch
,只要先前的約定中,有某個非同步操作發生了錯誤,就會背棄約定,從而執行 catch
指定的函式。
如果你有多個 Promise
,並不關心滿足約定的順序,只要最後的結果是按照指定約定的順序排列就可以時,可以使用 Promise.all
,它接受一個 Promise
組成的陣列,例如:
Promise.all([dividedRandom(10), dividedRandom(10), dividedRandom(10)])
.then(results => console.log(results[0], results[1], results[2]))
.catch(err => console.log(err));
如果你有多個 Promise
,並不關心哪一個約定先滿足,只要有個約定滿足就可以的話,可以使用 Promise.race
,它接受一個 Promise
組成的陣列,並傳回一個 Promise
,只要其中有個約定先滿足或背棄,傳回的 Promise
就算滿足或背棄約定,陣列中其他約定依舊會繼續其任務,只是無論滿足或背棄,都不被 Promise.race
傳回的 Promise
考量,一個使用 Promise.race
的例子是:
Promise.race([dividedRandom(10), dividedRandom(10), dividedRandom(10)])
.then(result => console.log(result))
.catch(err => console.log(err));
Promise
的 then
可以接受 Promise
,如果不在乎前一個 Promise
的結果,只需要在前一個 Promise
完成後執行時使用。例如:
asyncFoo(10)
.then(asyncFoo(20));
當約定與產生器結合時,可以產生有趣的操作風格,例如,若撰寫一個 async
函式如下:
function async(g) {
let it = g();
function consume(iteratorResult) {
if(iteratorResult.done) {
return;
}
let iteratorValue = iteratorResult.value;
if(iteratorValue instanceof Promise) {
iteratorValue.then(r => consume(it.next(r)))
.catch(err => it.throw(err));
} else {
it.throw(`${iteratorValue} not a promise`);
}
}
try {
consume(it.next());
} catch(err) {
it.throw(err);
}
}
async(function*() {
let r1 = yield asyncFoo(10);
let r2 = yield asyncFoo(r1);
let r3 = yield asyncFoo(r2);
console.log(r3);
});
跟一開始的這個風格比比看:
asyncFoo(10)
.then(r1 => asyncFoo(r1))
.then(r2 => asyncFoo(r2))
.then(r3 => console.log(r3));
async
的版本更像是語法層面的支援,實際上,在 ES8 中新增的 async
、await
就在語法層面上提供了這類支援:
async function task() {
let r1 = await asyncFoo(10);
let r2 = await asyncFoo(r1);
let r3 = await asyncFoo(r2);
console.log(r3);
}
task();
你不用自行撰寫一個 async
函式了,而 await
在語意上,也會比 yield
來得清楚。