構建一個應用程式總是會面對非同步呼叫,不論是在 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 社區 提問。