Skip to content

Latest commit

 

History

History
249 lines (194 loc) · 8.45 KB

02.md

File metadata and controls

249 lines (194 loc) · 8.45 KB

Javascript From Callback to Async/Await

Javascript 裡面的非同步實作上在不同版本有許多種實作方式,不外乎為 ES5 Callback, ES6 Promise, Generator, ES7 Async/Await。 在經過一連串的演化之下, Javascript 目前已經可以讓非同步的運作變得更直覺更簡潔, 並且也解決了太多的 callback 而產生的 callback hell。 本文章重點在於非同步上的演化過成。


Callback

簡單來說,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

通常會發生在需要多次非同步並且資料有相關性之下才會發生的。但 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 ,增加維護上的困難。


Promise

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 狂了)


Generator Function

不同於一般的 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 本身並不該包含 *

yield

這是 Generator function 最重要的精隨,它可以讓 function 有中斷(非同步)的效果。考試也考一百分了呢 當我們呼叫 generatorFoo 時,會得到一個 iterator,當我們每次呼叫這個 iterator 的 next 方法時,就會執行 generatorFoo,一直到出現 yield 關鍵字的地方,接下來會暫停,直到下次呼next。

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

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 下去即可,大大的增加了閱讀性。 是否是否是否


現在就開始用 Async/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

  1. https://noootown.wordpress.com/2016/11/13/callback-promise-fetch-yield-async-await/
  2. http://huli.logdown.com/posts/292655-javascript-promise-generator-async-es6
  3. https://denny.qollie.com/2016/05/08/es6-generator-func/
  4. https://medium.com/@peterchang_82818/javascript-es7-async-await-%E6%95%99%E5%AD%B8-703473854f29-tutorial-example-703473854f29
  5. https://jigsawye.com/2016/04/18/understanding-javascript-async-await/

tags: Javascript callback Promise Generator Async Await