JavaScript ES7 中使用 async/await 解決回調(diào)函數(shù)嵌套問(wèn)題(摘抄作者:asaka)

JavaScript 中最蛋疼的事情莫過(guò)于回調(diào)函數(shù)嵌套問(wèn)題。以往在瀏覽器中,因?yàn)榕c服務(wù)器通訊是一種比較昂貴的操作,因此比較復(fù)雜的業(yè)務(wù)邏輯往往都放在服務(wù)器端,前端 JavaScript 只需要少數(shù)幾次 AJAX 請(qǐng)求就可拿到全部數(shù)據(jù)。

但是到了 webapp 風(fēng)行的時(shí)代,前端業(yè)務(wù)邏輯越來(lái)越復(fù)雜,往往幾個(gè) AJAX 請(qǐng)求之間互有依賴,有些請(qǐng)求依賴前面請(qǐng)求的數(shù)據(jù),有些請(qǐng)求需要并行進(jìn)行。還有在類(lèi)似 node.js 的后端 JavaScript 環(huán)境中,因?yàn)樾枰M(jìn)行大量 IO 操作,問(wèn)題更加明顯。這個(gè)時(shí)候使用回調(diào)函數(shù)來(lái)組織代碼往往會(huì)導(dǎo)致代碼難以閱讀。

現(xiàn)在比較流行的解決這個(gè)問(wèn)題的方法是使用 Promise,可以將嵌套的回調(diào)函數(shù)展平。但是寫(xiě)代碼和閱讀依然有額外的負(fù)擔(dān)。

另外一個(gè)方案是使用 ES6 中新增的 generator,因?yàn)?generator 的本質(zhì)是可以將一個(gè)函數(shù)執(zhí)行暫停,并保存上下文,再次調(diào)用時(shí)恢復(fù)當(dāng)時(shí)的狀態(tài)。co 模塊是個(gè)不錯(cuò)的封裝。但是這樣略微有些濫用 generator 特性的感覺(jué)。

ES7 中有了更加標(biāo)準(zhǔn)的解決方案,新增了 async/await 兩個(gè)關(guān)鍵詞。async 可以聲明一個(gè)異步函數(shù),此函數(shù)需要返回一個(gè) Promise 對(duì)象。await 可以等待一個(gè) Promise 對(duì)象 resolve,并拿到結(jié)果。
比如下面的例子,以往我們無(wú)法在 JavaScript 中使用常見(jiàn)的 sleep 函數(shù),只能使用 setTimeout 來(lái)注冊(cè)一個(gè)回調(diào)函數(shù),在指定的時(shí)間之后再執(zhí)行。有了 async/await 之后,我們就可以這樣實(shí)現(xiàn)了:

async 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());
})();

執(zhí)行此段代碼,可以在終端中看到結(jié)果:

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 函數(shù)可以正常的返回結(jié)果和拋出異常。await 函數(shù)調(diào)用即可拿到結(jié)果,在外面包上 try/catch 就可以捕獲異常。下面是一個(gè)從豆瓣 API 獲取數(shù)據(jù)的例子:

var 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);
  }
})();

注意:

await 命令后面的 Promise 對(duì)象,運(yùn)行結(jié)果可能是 rejected,所以最好把 await 命令放在 try...catch 代碼塊中。

async function myFunction() {
  try {
    await somethingThatReturnsAPromise();
  } catch (err) {
    console.log(err);
  }
}

// 另一種寫(xiě)法
async function myFunction() {
  await somethingThatReturnsAPromise().catch(function (err){
    console.log(err);
  });
}

指定多少毫秒后輸出一個(gè)值。代碼如下:

function timeout(ms) {
     return new Promise((resolve) => {
         setTimeout(resolve, ms);
     });
 }

async function asyncPrint(value, ms) {
     await timeout(ms);
     console.log(value)
}

asyncPrint('hello world', 2000);
結(jié)果是2秒后在控制臺(tái)上輸出:
hello world

那使用async/await前需要準(zhǔn)備什么?

babel已經(jīng)支持async的transform了,所以我們使用的時(shí)候引入babel就行。當(dāng)然server端和browser端,可以有不同的處理方法。在開(kāi)始之前我們需要引入以下的package, preset-stage-3 里就有我們需要的async/await的編譯文件。

$ npm install babel-core --save
$ npm install babel-preset-es2015 --save
$ npm install babel-preset-stage-3 --save
Browser端

Babel一開(kāi)始的出現(xiàn)就是為了讓舊瀏覽器也能支持新的ES6特性,提升我們的開(kāi)發(fā)體驗(yàn)。所以在Babel一開(kāi)始就是可以通過(guò)babel-cli終端進(jìn)行編譯的。或者引入babel文件在瀏覽器端進(jìn)行編譯。當(dāng)然這些都不是我最推薦的,所以我就帶過(guò)不說(shuō)啦。在前端靜態(tài)資源配置里,webpack是現(xiàn)在比較好的解決方案,它支持靜態(tài)資源的模塊依賴,打包合并,還有語(yǔ)言的預(yù)處理,當(dāng)然在這里我們就是指babel的處理。

// webpack.config.js
// 省略上面的文件輸入輸出的配置,直接看模塊加載器的配置
module: {
    loaders: [
        {
            test: /\.js$/,
            exclude: /(node_modules|bower_components)/,
            loader: "babel",
            query: {
              presets: ['es2015', 'stage-3']
            }
        },
    ]
}

Server端

相對(duì)來(lái)說(shuō),后端比前端需要處理的異步IO地方多得多,也是更加需要這個(gè)。那我們?cè)赟erver端又如何引入babel呢?

其實(shí)最簡(jiǎn)單也是最麻煩的方法就是,直接把js文件通過(guò)babel編譯出新的文件再來(lái)使用。當(dāng)然也就免不了冗余文件了,眼不見(jiàn)心不煩,還是換一個(gè)方法吧。

我們使用官方提供的require hook方法,顧名思義就是通過(guò)require進(jìn)來(lái)后,接下來(lái)的文件進(jìn)行require的時(shí)候都會(huì)經(jīng)過(guò)Babel的處理。因?yàn)槲覀冎繡ommonJs是同步的模塊依賴,所以也是可行的方法。我們需要多一個(gè)用于啟動(dòng)的js文件,一個(gè)真正執(zhí)行程序的js文件。

// index.js 
// 用于引入babel,并且啟動(dòng)app.js

require("babel-core/register");
require("./app.js");

配置完hook之后,我們就配置babel的.babelrc文件,它是一個(gè)json格式的文件。es2015看情況配置,如果是已經(jīng)是Node5.0版本,就無(wú)需再進(jìn)行編譯。

{
  "presets": ["stage-3", "es2015"]
}

ES7 還在草案階段,那現(xiàn)在想用這個(gè)特性怎么辦?可以嘗試 google 的一個(gè) JavaScript 預(yù)編譯器 traceur,可以將高版本的 JavaScript 編譯為 ES5 代碼,已經(jīng)實(shí)驗(yàn)性的支持了 async/await (需要使用 --experimental 來(lái)指定開(kāi)啟)。traceur 可以直接在后端使用,也可以在瀏覽器中使用。另外如果只在 node.js 環(huán)境中使用的話,還有一些 polyfill 模塊,比如這個(gè)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容