在實際開發中總會遇到許多非同步的問題,最常見的場景介面請求之後一定要等一段時間才能得到結果,如果遇到多個介面前後依賴,那麼問題就變得複雜。大家都一直在嘗試使用更好的方案來解決這些問題。最開始只能利用回撥函式,後來開始有人使用Promise的思維來搞定。到ES6中開始支援原生的Promise,引入Generator函式。
直到ES7,有了async/await。
這是一個用同步的思維來解決非同步問題的方案。
我想很多人可能還不太分得清同步與非同步的區別。如果你已經徹底瞭解了事件迴圈,那麼想必對非同步的概念應該非常瞭解。當我們發出了請求,並不會等待響應結果,而是會繼續執行後面的程式碼,響應結果的處理在之後的事件迴圈中解決。那麼同步的意思,就是等結果出來之後,程式碼才會繼續往下執行。
我們可以用一個兩人問答的場景來比喻非同步與同步。A向B問了一個問題之後,不等待B的回答,接著問下一個問題,這是非同步。A向B問了一個問題之後,然後就笑呵呵的等著B回答,B回答了之後他才會接著問下一個問題。
那麼我們先記住這個特點,async/await
使用同步的思維,來解決非同步的問題。在繼續講解它的語法與使用之前,我們先介紹一下如何在我們的開發環境中支援該語法。
如果你已經知道如何配置,可跳過
一、如何在自己的開發環境中支援async/await
語法
這裡主要介紹兩種方式。
1. webpack中支援該語法
首先在當前專案中使用npm下載babel-loader
。
1 |
> npm install babel-loader --save-dev |
然後在配置檔案webpack.confing.dev.js
中配置,在module.exports.module.rules
中新增如下配置元素即可。
1 2 3 4 5 6 7 8 |
{ test: /\.(js|jsx)$/, include: paths.appSrc, loader: require.resolve('babel-loader'), options: { cacheDirectory: true, }, }, |
如果你使用最新版本的create-react-app或者vue-cli來構建你的程式碼,那麼它們應該已經支援了該配置。
2. gulp中支援該語法
首先安裝gulp外掛
1 |
> npm install gulp-babel --save-dev |
然後編寫任務
1 2 3 4 5 6 7 8 |
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')); }); |
二、如何使用
async函式是Generator的一個語法糖。如果你不知道Generator是什麼函式也沒有關係,我們只需要知道async函式實際上返回的是一個Promise物件即可。
1 2 3 4 5 6 7 8 |
async function fn() { return 30; } // 或者 const fn = async () => { return 30; } |
在宣告函式時,前面加上關鍵字async
,這就是async
的用法。當我們用console.log
列印出上面宣告的函式fn,我們可以看到如下結果:
1 2 3 4 5 6 7 8 |
console.log(fn()); // result Promise = { __proto__: Promise, [[PromiseStatus]]: "resolved", [[PromiseValue]]: 30 } |
很顯然,fn的執行結果其實就是一個Promise物件。因此我們也可以使用then來處理後續邏輯。
1 2 3 |
fn().then(res => { console.log(res); // 30 }) |
await的含義為等待。意思就是程式碼需要等待await後面的函式執行完並且有了返回結果之後,才繼續執行下面的程式碼。這正是同步的效果。
但是我們需要注意的是,await關鍵字只能在async函式中使用。並且await後面的函式執行後必須返回一個Promise物件才能實現同步的效果。
當我們使用一個變數去接收await的返回值時,該返回值為Promise中resolve出來的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// 定義一個返回Promise物件的函式 function fn() { return new Promise((resolve, reject) => { setTimeout(() => { resolve(30); }, 1000); }) } // 然後利用async/await來完成程式碼 const foo = async () => { const t = await fn(); console.log(t); console.log('next code'); } foo(); // result: // 30 // next code |
執行這個例子我們可以看出,當在async函式中,執行遇到await時,就會等待await後面的函式執行完畢,而不會直接執行next code
。
如果我們直接使用then方法的話,想要達到同樣的結果,就不得不把後續的邏輯寫在then方法中。
1 2 3 4 5 6 7 8 |
const foo = () => { return fn().then(t => { console.log(t); console.log('next code'); }) } foo(); |
很顯然如果使用async/await的話,程式碼結構會更加簡潔,邏輯也更加清晰。
異常處理
在Promise中,我們知道是通過catch的方式來捕獲異常。而當我們使用async時,則通過try/catch
來捕獲異常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function fn() { return new Promise((resolve, reject) => { setTimeout(() => { reject('some error.'); }, 1000); }) } const foo = async () => { try { await fn(); } catch (e) { console.log(e); // some error } } foo(); |
如果有多個await函式,那麼只會返回第一個捕獲到的異常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
function fn1() { return new Promise((resolve, reject) => { setTimeout(() => { reject('some error fn1.'); }, 1000); }) } function fn2() { return new Promise((resolve, reject) => { setTimeout(() => { reject('some error fn2.'); }, 1000); }) } const foo = async () => { try { await fn1(); await fn2(); } catch (e) { console.log(e); // some error fn1. } } foo(); |
實踐
在實踐中我們遇到非同步場景最多的就是介面請求,那麼這裡就以jquery中的$.get
為例簡單展示一下如何配合async/await
來解決這個場景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 先定義介面請求的方法,由於jquery封裝的幾個請求方法都是返回Promise例項,因此可以直接使用await函式實現同步 const getUserInfo = () => $.get('xxxx/api/xx'); const clickHandler = async () => { try { const resp = await getUserInfo(); // resp為介面返回內容,接下來利用它來處理對應的邏輯 console.log(resp); // do something } catch (e) { // 處理錯誤邏輯 } } |
為了保證邏輯的完整性,在實踐中
try/catch
必不可少。總之,不處理錯誤邏輯的程式設計師不是好程式設計師。
與Promise相比,個人認為async/await
有一定的簡潔性,但也並非就比Promise有絕對的優勢,因此只能算是提供了另外一種稍好的方式,至於大家學習之後選擇哪種方式來解決自己的問題,這僅僅只是你的個人喜好問題。