JavaScript ES7 中使用 async/await 解決回撥函式巢狀問題

江澤民發表於2015-02-26

JavaScript 中最蛋疼的事情莫過於回撥函式巢狀問題。以往在瀏覽器中,因為與伺服器通訊是一種比較昂貴的操作,因此比較複雜的業務邏輯往往都放在伺服器端,前端 JavaScript 只需要少數幾次 AJAX 請求就可拿到全部資料。

但是到了 webapp 風行的時代,前端業務邏輯越來越複雜,往往幾個 AJAX 請求之間互有依賴,有些請求依賴前面請求的資料,有些請求需要並行進行。還有在類似 node.js 的後端 JavaScript 環境中,因為需要進行大量 IO 操作,問題更加明顯。這個時候使用回撥函式來組織程式碼往往會導致程式碼難以閱讀。

現在比較流行的解決這個問題的方法是使用 Promise,可以將巢狀的回撥函式展平。但是寫程式碼和閱讀依然有額外的負擔。

另外一個方案是使用 ES6 中新增的 generator,因為 generator 的本質是可以將一個函式執行暫停,並儲存上下文,再次呼叫時恢復當時的狀態。co 模組是個不錯的封裝。但是這樣略微有些濫用 generator 特性的感覺。

ES7 中有了更加標準的解決方案,新增了 async/await 兩個關鍵詞。async 可以宣告一個非同步函式,此函式需要返回一個 Promise 物件。await 可以等待一個 Promise 物件 resolve,並拿到結果。

比如下面的例子,以往我們無法在 JavaScript 中使用常見的 sleep 函式,只能使用 setTimeout 來註冊一個回撥函式,在指定的時間之後再執行。有了 async/await 之後,我們就可以這樣實現了:

jsasync function sleep(timeout) {
  return new Promise((resolve, reject) => {
    setTimeout(function() {
      resolve();
    }, timeout);
  });
}

(async function() {
  console.log('Do some thing, ' + new Date());
  await sleep(3000);
  console.log('Do other things, ' + new Date());
})();

執行此段程式碼,可以在終端中看到結果:

Do some thing, Mon Feb 23 2015 21:52:11 GMT+0800 (CST)
Do other things, Mon Feb 23 2015 21:52:14 GMT+0800 (CST)

另外 async 函式可以正常的返回結果和丟擲異常。await 函式呼叫即可拿到結果,在外面包上 try/catch 就可以捕獲異常。下面是一個從豆瓣 API 獲取資料的例子:

jsvar fetchDoubanApi = function() {
  return new Promise((resolve, reject) => {
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4) {
        if (xhr.status >= 200 && xhr.status < 300) {
          var response;
          try {
            response = JSON.parse(xhr.responseText);
          } catch (e) {
            reject(e);
          }
          if (response) {
            resolve(response, xhr.status, xhr);
          }
        } else {
          reject(xhr);
        }
      }
    };
    xhr.open('GET', 'https://api.douban.com/v2/user/aisk', true);
    xhr.setRequestHeader("Content-Type", "text/plain");
    xhr.send(data);
  });
};

(async function() {
  try {
    let result = await fetchDoubanApi();
    console.log(result);
  } catch (e) {
    console.log(e);
  }
})();

ES7 還在草案階段,那現在想用這個特性怎麼辦?可以嘗試 google 的一個 JavaScript 預編譯器 traceur,可以將高版本的 JavaScript 編譯為 ES5 程式碼,已經實驗性的支援了 async/await (需要使用 --experimental 來指定開啟)。traceur 可以直接在後端使用,也可以在瀏覽器中使用。另外如果只在 node.js 環境中使用的話,還有一些 polyfill 模組,比如這個

更多文章參見: http://aisk.me/using-async-await-to-avoid-callback-hell/

相關文章