Javascript 裡面的非同步實作上在不同版本有許多種實作方式,不外乎為 ES5 Callback, ES6 Promise, Generator, ES7 Async/Await。 在經過一連串的演化之下, Javascript 目前已經可以讓非同步的運作變得更直覺更簡潔, 並且也解決了太多的 callback 而產生的 callback hell。 本文章重點在於非同步上的演化過成。
簡單來說,callback 就是把 A function 傳進另一個 B function,當 B function 做完事後,就 call A function,做它該做的事。
假設政府提供一個查颱風即時動態的 API 好了,然後拿回資料後,我們可以在網頁上把它的位置畫出來。那麼程式碼如下:
function drawRedTyphoon(result) {
draw(result, 'red')
}
function drawBlueTyphoon(result) {
draw(result, 'blue')
}
function manipulateTyphoon(cb) {
$.ajax("www.president.gov.tw/api/typhoon",{})
.done(cb(result))
}
這邊傳進去的 cb 就是一個 callback function,根據不同的 callback,我們可以做不同的事。
通常會發生在需要多次非同步並且資料有相關性之下才會發生的。但 callback hell 不是一個 bug,也不會造成程式上有任何問題。但因為會需要多層 callback 架構,所以造成程式不易維護。
假設政府提供一個查颱風歷史清單的 API,跟查詢單一颱風詳細資料的 API。那麼程式碼如下:
function main() {
getTyphoonList((result) => {
getTyphoonInfo(()=> {
console.log("done");
} result.id)
})
}
function getTyphoonList(cb) {
$.ajax("www.president.gov.tw/api/getTyphoonList", {})
.done(cb(result))
}
function getTyphoonInfo(cb, id) {
$.ajax("www.president.gov.tw/api/getTyphoonInfo", {id: id})
.done(cb(result))
}
在 main 裡面,可以看到有兩層的 callback,還算是好維護。但在讀寫檔案、資料庫、API 的溝通,可能會有多層 callback,此時就會造成所謂的 callback hell ,增加維護上的困難。
ES6 Promise 的實作中,會確保 Promise 物件一實體化後就會固定住狀態,要不就是 已實現,要不就是 已拒絕
一個簡單的 Promise 物件如下:
const promise = new Promise(function(resolve, reject) {
resolve(value) // 成功時
reject(reason) // 失敗時
});
promise.then(function(value) {
// on fulfillment(已實現時)
}, function(reason) {
// on rejection(已拒絕時)
})
若使用 Promise 物件來改寫取得颱風即時動態的例子,就可以如下:
function main() {
getTyphoon().then(console.log("done")).catch(console.error(reason));
}
function getTyphoon() {
return new Promise((resolve, reject) => {
$.ajax("www.president.gov.tw/api/typhoon",{})
.success(result => resolve(result))
.fail(reason => reject(reason))
})
}
在使用 Promise 的情況之下,就可以把 callback 使用 then 代替,並且也可以使用 catch 達到 error handling 了。
但如果有很多 Promise 的物件,物件之間彼此需要資料上的交換。則程式中會有許多的 then/catch,進一步的造成閱讀上的困難。(但已經比 callback 狂了)
不同於一般的 function (或稱 run to complete function) Generator function 特別的地方就是它可以被暫停,等到下次進來時再繼續呼叫它。
先看一個簡單的 Generator function的例子:
function* generatorFoo() {
yield "手";
yield "起";
yield "刀";
yield "落";
}
let g = generatorFoo();
let a = g.next()
console.log(a.value) // “手”
a = g.next()
console.log(a.value) // “起”
a = g.next()
console.log(a.value) // “刀”
a = g.next()
console.log(a.value) // “落”
從上面的例子,可以看到 function 內沒有任何的 return 。而且多了一個 * 在 function 的宣告上。 沒錯這就是 generator function 的宣告式。
有人會爭論 * 到底是要放在 function 關鍵字後面,還是直接放在 function 名字前面 e.q: function *generatorFoo 兩個都是合格的語法,不過我習慣放在 function 關鍵字後面,我認為這是個不同的 function,而且 function name 本身並不該包含 *
這是 Generator function 最重要的精隨,它可以讓 function 有中斷(非同步)的效果。考試也考一百分了呢
當我們呼叫 generatorFoo 時,會得到一個 iterator,當我們每次呼叫這個 iterator 的 next 方法時,就會執行 generatorFoo,一直到出現 yield 關鍵字的地方,接下來會暫停,直到下次呼next。
next 會返回一個物件,裡面包含著兩個 properties,分別是 value 和 done:
- value:就是我們在前一段中從 yield 那個位置,接到的「值」。
- done:boolean 值,假如這個 Generator function 完全被執行完的話,done 就會變成 true,反之亦然。
done property 值得注意的是:
- 當執行到最後一個 yield 時,done 仍然會是 false,再執行一次才會得到 done 為 true。
- 我們可以在 funcion 裡面 return 東西,如此在執行到 return 這一行時,next 就會返回 value 為 return 的東西,並且 done 為 true。
若使用 Generator function 來改寫取得颱風即時動態的例子,就可以如下:
function main() {
let r = run();
r.next().value((result) => (console.log("done")));
}
function* run() {
yield getTyphoon();
}
function getTyphoon() {
return new Promise((resolve, reject) => {
$.ajax("www.president.gov.tw/api/typhoon",{})
.success(result => resolve(result))
.fail(reason => reject(reason))
})
}
由此範例可以看到 Generator 的特性,寫出超級像同步但其實是非同步的程式碼。並且結合了 Promise。來達到非同步的程式碼。但我不喜歡用他
Async/Await 被規範在 ES2016 的標準中,很多的討論都指向 Async/Await 會是非同步的終極解決方案。
先來看看他的函式宣告:
async function asyncFoo() {
let result = await foo(); //Something need to wait for result
}
async 為宣告瀏覽器說 此 function 為非同步喔。 await 表示要等待這個非同步的結果回傳後才會繼續執行。 進程達到 function 為同步的樣子惹。
若如果需要做 error handling。則可以很方便的使用 try/catch:
async function asyncFoo() {
try {
let result = await foo(); //Something need to wait for result
}
catch(error) {
console.error(error);
}
}
若使用 Async/Await 來改寫取得颱風即時動態的例子,就可以如下:
async function main() {
let t = await getTyphoon();
}
function getTyphoon() {
return new Promise((resolve, reject) => {
$.ajax("www.president.gov.tw/api/typhoon",{})
.success(result => resolve(result))
.fail(reason => reject(reason))
})
}
這樣就變得非常的簡易了,並且如果需要多個非同步函式,則只需要繼續 await 下去即可,大大的增加了閱讀性。 是否是否是否
ES7 Async/Await 大大的減少了非同步函式的複雜性,增加了可維護性與閱讀性。可以說是 modern web 必備的能力。 此時不用待何時
其實如果有在寫 Front-end(尤其是 React),基本上應該已經使用了 babel。如果要使用 Async/Await,presets 除了原本的 es2015 外,只要加上 stage-3:
.bebalrc
{
"presets": ["es2015", "stage-3"]
}
或是將 transform-async-to-generator 加入 plugins 就行了:
.bebalrc
{
"presets": ["es2015"],
"plugins": ["transform-async-to-generator"]
}
Node 7.0.0 起已經支援 Async/Await,建議直接更新你的 Node 版本!
Reference
- https://noootown.wordpress.com/2016/11/13/callback-promise-fetch-yield-async-await/
- http://huli.logdown.com/posts/292655-javascript-promise-generator-async-es6
- https://denny.qollie.com/2016/05/08/es6-generator-func/
- https://medium.com/@peterchang_82818/javascript-es7-async-await-%E6%95%99%E5%AD%B8-703473854f29-tutorial-example-703473854f29
- https://jigsawye.com/2016/04/18/understanding-javascript-async-await/