目前最好的 JavaScript 異步方案 async/await

jopen 8年前發布 | 26K 次閱讀 Node.js JavaScript開發 ECMAScript

構建一個應用程序總是會面對異步調用,不論是在 Web 前端界面,還是 Node.js 服務端都是如此,JavaScript 里面處理異步調用一直是非常惡心的一件事情。以前只能通過回調函數,后來漸漸又演化出來很多方案,最后 Promise 以簡單、易用、兼容性好取勝,但是仍然有非常多的問題。其實 JavaScript 一直想在語言層面徹底解決這個問題,在 ES6 中就已經支持原生的 Promise,還引入了 Generator 函數,終于在 ES7 中決定支持 async 和 await。

基本語法

async/await 究竟是怎么解決異步調用的寫法呢?簡單來說,就是將異步操作用同步的寫法來寫。先來看下最基本的語法(ES7 代碼片段):

const f = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(123);
    }, 2000);
  });
};

const testAsync = async () => {
  const t = await f();
  console.log(t);
};

testAsync();

首先定義了一個函數 f ,這個函數返回一個 Promise,并且會延時 2 秒, resolve 并且傳入值 123。 testAsync 函數在定義時使用了關鍵字 async ,然后函數體中配合使用了 await ,最后執行 testAsync 。整個程序會在 2 秒后輸出 123,也就是說 testAsync 中常量 t 取得了 f 中 resolve 的值,并且通過 await 阻塞了后面代碼的執行,直到 f 這個異步函數執行完。

對比 Promise

僅僅是一個簡單的調用,就已經能夠看出來 async/await 的強大,寫碼時可以非常優雅地處理異步函數,徹底告別回調惡夢和無數的 then 方法。我們再來看下與 Promise 的對比,同樣的代碼,如果完全使用 Promise 會有什么問題呢?

const f = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(123);
    }, 2000);
  });
};

const testAsync = () => {
  f().then((t) => {
    console.log(t);
  });
};

testAsync();

從代碼片段中不難看出 Promise 沒有解決好的事情,比如要有很多的 then 方法,整塊代碼會充滿 Promise 的方法,而不是業務邏輯本身,而且每一個 then 方法內部是一個獨立的作用域,要是想共享數據,就要將部分數據暴露在最外層,在 then 內部賦值一次。雖然如此,Promise 對于異步操作的封裝還是非常不錯的,所以 async/await 是基于 Promise 的, await 后面是要接收一個 Promise 實例。

對比 RxJS

RxJS 也是非常有意思的東西,用來處理異步操作,它更能處理基于流的數據操作。舉個例子,比如在 Angular2 中 http 請求返回的就是一個 RxJS 構造的 Observable Object,我們就可以這樣做:

$http.get(url)
  .map(function(value) {
    return value + 1;
  })
  .filter(function(value) {
    return value !== null;
  })
  .forEach(function(value) {
    console.log(value);
  })
  .subscribe(function(value) {
    console.log('do something.');
  }, function(err) {
    console.log(err);
  });

如果是 ES6 代碼可以進一步簡潔:

$http.get(url) 
  .map(value => value + 1) 
  .filter(value => value !== null) 
  .forEach(value => console.log(value)) 
  .subscribe((value) => { 
    console.log('do something.'); 
  }, (err) => { 
    console.log(err); 
  }); 

可以看出 RxJS 對于這類數據可以做一種類似流式的處理,也是非常優雅,而且 RxJS 強大之處在于你還可以對數據做取消、監聽、節流等等的操作,這里不一一舉例了,感興趣的話可以去看下 RxJS 的 API。

這里要說明一下的就是 RxJS 和 async/await 一起用也是可以的,Observable Object 中有 toPromise 方法,可以返回一個 Promise Object,同樣可以結合 await 使用。當然你也可以只使用 async/await 配合 underscore 或者其他庫,也能實現很優雅的效果。總之,RxJS 與 async/await 不沖突。

異常處理

通過使用 async/await,我們就可以配合 try/catch 來捕獲異步操作過程中的問題,包括 Promise 中 reject 的數據。

const f = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(234);
    }, 2000);
  });
};

const testAsync = () => {
  try {
    const t = await f();
    console.log(t);
  } catch (err) {
    console.log(err);
  }
};

testAsync();

代碼片段中將 f 方法中的 resolve 改為 reject ,在 testAsync 中,通過 catch 可以捕獲到 reject 的數據,輸出 err 的值為 234。 try/catch 使用時也要注意范圍和層級。如果 try 范圍內包含多個 await ,那么 catch 會返回第一個 reject 的值或錯誤。

const f1 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(111);
    }, 2000);
  });
};

const f2 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(222);
    }, 3000);
  });
};

const testAsync = () => {
  try {
    const t1 = await f1();
    console.log(t1);
    const t2 = await f2();
    console.log(t2);
  } catch (err) {
    console.log(err);
  }
};

testAsync();

如代碼片段所示, testAsync 函數體中 try 有兩個 await 函數,而且都分別 reject ,那么 catch 中僅會觸發 f1 的 reject ,輸出的 err 值是 111。

開始使用

無論是 Web 前端還是 Node.js 服務端,都可以通過預編譯的手段實現使用 ES6 和 ES7 來寫代碼,目前最流行的方案是通過 Babel 將使用 ES7、ES6 寫的代碼編譯為 E6 或 ES5 的代碼來執行。

Node.js 服務端配置

服務端使用 Babel,最簡單的方式是通過 require hook。

首先安裝 Babel:

$ npm install babel-core --save

安裝 async/await 支持:

$ npm install babel-preset-stage-3 --save

在服務端代碼的根目錄中配置 .babelrc 文件,內容為:

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

在頂層代碼文件(server.js 或 app.js 等)中引入 Babel 模塊:

require("babel-core/register");

在這句后面引入的模塊,都將會自動通過 babel 編譯,但當前文件不會被 babel 編譯。另外,需要注意 Node.js 的版本,如果是 4.0 以上的版本則默認支持絕大部分 ES6,可以直接啟動。但是如果是 0.12 左右的版本,就需要通過 node —harmory 來啟動才能夠支持。因為 stage-3 模式,Babel 不會編譯基本的 ES6 代碼,環境既然支持又何必要編譯為 ES5?這樣做也是為了提高性能和編譯效率。

配置 Web 前端構建

可以通過增加 Gulp 的預編譯 task 來支持。

首先安裝 gulp-babel 插件:

$ npm install gulp-babel --save-dev

然后編寫配置:

var gulp = require('gulp');
var babel = require('gulp-babel');

gulp.task('babel', function() {
  return gulp.src('src/app.js')
    .pipe(babel())
    .pipe(gulp.dest('dist'));
});

除了 Gulp-babel 插件,也可以使用官方的 Babel-loader 結合 Webpack 或 Browserify 使用。

要注意的是,雖然官方也有純瀏覽器版本的 Babel.js,但是瀏覽器限制非常多,而且對客戶端性能影響也較大,不推薦使用。

LeanEngine Full Stack

LeanEngine(云引擎)是LeanCloud 推出的服務器端運行環境,支持 Node.js 和 Python 環境,功能強大而且目前免費,結合 LeanCloud JavaScript SDK,使原本復雜的開發工作變得簡單高效。目前也支持 Redis 和海外節點,輕松滿足你的業務需求。

LeanCloud 使用自已的服務編寫出了很多的應用和 Web 產品。為了方便各位開發者基于 LeanEngine 來開發應用,LeanCloud 整理了目前開發 Web 端產品的技術棧,并結合 LeanEngine 特點,推出了一套完整實用的技術解決方案: LeanEngine-Full-Stack ,它已經配置了 Babel,可以在 LeanEngine 中結合 JavaScript SDK 使用 async/await 處理異步操作。所以,還等什么?快來下載編寫新項目吧。

Enjoy!

結語

LeanCloud 希望能夠通過構建最簡單易用的技術產品,幫助各位開發者和創業者加速產品開發,盡可能地節約資源成本、時間成本和機會成本,希望本文能夠幫助到你。有什么問題,可以在 LeanEngine-Full-Stack @GitHub 倉庫 中提交 issue,或者直接去LeanCloud 社區 提問。

【參考資料】 EcmaScript async/await 詳細規范

來自: https://blog.leancloud.cn/3910/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!