一邊學習前端,一邊通過部落格的形式自己總結一些東西,當然也希望幫助一些和我一樣開始學前端的小夥伴。
如果出現錯誤,請在評論中指出,我也好自己糾正自己的錯誤
author: thomaszhou
讓我們開始學習async和await
async/await使用同步的思維,來解決非同步的問題。
-
async的優點
利用async建立的函式也是非同步函式,就像setTimeout那種一樣
async/await 的優勢在於處理 then 鏈
:- 如果需要處理由多個 Promise 組成的 then 鏈的時候,優勢就能體現出來了,因為promise引數傳遞太麻煩了,而async/await特別方便
- async可以直接接收傳遞的變數,但是peomise的then是獨立作用於,如果要取值,就要將部分資料暴露在最外層,在 then 內部賦值一次.
-
相比較generator
- (1)內建執行器。 Generator 函式的執行必須靠執行器,所以才有了 co 函式庫,而 async 函式自帶執行器。也就是說,async 函式的執行,與普通函式一模一樣,只要一行。
- (2)更好的語義。 async 和 await,比起星號和 yield,語義更清楚了。async 表示函式裡有非同步操作,await 表示緊跟在後面的表示式需要等待結果。
- (3)更廣的適用性。 co 函式庫約定,yield 命令後面只能是 Thunk 函式或 Promise 物件,而async 函式的 await 命令後面,可以跟 Promise 物件和原始型別的值(數值、字串和布林值,但這時等同於同步操作)。
同步和非同步的理解:當我們發出了請求,並不會等待響應結果,而是會繼續執行後面的程式碼,響應結果的處理在之後的事件迴圈中解決。那麼同步的意思,就是等結果出來之後,程式碼才會繼續往下執行。
我們可以用一個兩人問答的場景來比喻非同步與同步。A向B問了一個問題之後,不等待B的回答,接著問下一個問題,這是非同步。A向B問了一個問題之後,然後就笑呵呵的等著B回答,B回答了之後他才會接著問下一個問題,這是同步。
1、安裝支援
babel已經支援,所以我們可以在webpack中使用 首先在當前專案中使用npm下載babel-loader。
npm install babel-loader --save-dev
複製程式碼
然後在配置文件webpack.confing.dev.js中配置
,在module.exports.module.rules中
新增如下配置元素即可。
{
test: /\.(js|jsx)$/,
include: paths.appSrc,
loader: require.resolve('babel-loader'),
options: {
cacheDirectory: true,
},
},
複製程式碼
如果你使用最新版本的create-react-app或者vue-cli來構建你的程式碼,那麼它們應該已經支援了該配置。
2、普通宣告和await使用
- async函式實際上返回的是一個Promise物件
async function fn() {
return 30;
}
// 或者
const fn = async () => {
return 30;
}
複製程式碼
在宣告函式時,前面加上關鍵字async,這就是async的用法。當我們用console.log列印出上面宣告的函式fn,我們可以看到如下結果:
console.log(fn());
//result
Promise = {
__proto__: Promise,
[[PromiseStatus]]: "resolved",
[[PromiseValue]]: 30
}
複製程式碼
很顯然,fn的執行結果其實就是一個Promise物件。因此我們也可以使用then來處理後續邏輯。
fn().then(res => {
console.log(res); // 30
})
複製程式碼
await的使用-------------------
await的含義為等待。就是程式碼需要等待await後面的函式執行完並且有了返回結果之後,才繼續執行下面的程式碼。這正是同步的效果。
但是我們需要注意的是,await關鍵字只能在async函式中使用。並且await後面的函式執行後必須返回一個Promise物件才能實現同步的效果。
當我們使用一個變數去接收await的返回值時,如:const temp = await fn();
該返回值temp為Promise中resolve出來的值(也就是PromiseValue)。
// 定義一個返回Promise物件的函式
function fn() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(30);
}, 1000);
})
}
// 然後利用async/await來完成程式碼
const foo = async () => {
const t = await fn(); // 將30傳入
console.log(t);
}
foo();
console.log('begin')
// begin
// 30
複製程式碼
首先我們定義了一個函式fn(),這個函式返回Promise,並且會延時 1 秒,resolve並且傳入值30,foo函式在定義時使用了關鍵字async,然後函式體中配合使用了await,最後執行foo()。整個程式會在 1 秒後輸出30,也就是說foo()中常量t取得了fn()中resolve的值,並且通過await阻塞了後面的程式碼執行,直到fn()這個非同步函式執行完。
執行這個例子我們可以看出,當在async函式中,執行遇到await時,就會等待await後面的函式執行完畢,而不會直接執行next code。
可以看到begin優先輸出,是因為async/await建立的foo()函式也是非同步函式,所以你懂的
如果我們直接使用promise的then方法的話,想要達到同樣的結果,就不得不把後續的邏輯寫在then方法中。
const foo = () => {
return fn().then(t => {
console.log(t);
console.log('next code');
})
}
foo();
複製程式碼
很顯然如果使用async/await的話,程式碼結構會更加簡潔,邏輯也更加清晰。
從程式碼片段中不難看出 Promise 沒有解決好的事情,比如要有很多的 then 方法,整塊程式碼會充滿 Promise 的方法,而不是業務邏輯本身.
而且每一個 then 方法內部是一個獨立的作用域,要是想共享資料,就要將部分資料暴露在最外層,在 then 內部賦值一次.
雖然如此,Promise 對於非同步操作的封裝還是非常不錯的,所以 async/await 是基於 Promise 的,await 後面是要接收一個 Promise 例項。
3、異常處理
在Promise中,我們知道是通過catch的方式來捕獲異常。而當我們使用async時,則通過try/catch來捕獲異常。
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 命令後面的 Promise 物件,執行結果可能是 rejected,所以最好把 await 命令放在 try...catch 程式碼塊中。
如果有多個await函式,那麼只會返回第一個捕獲到的異常。
function fn1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('some error fn1.');// 設定reject
}, 1000);
})
}
function fn2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('some error fn2.'); // 設定reject
}, 1000);
})
}
const foo = async () => {
try {
await fn1();
await fn2();
} catch (e) {
console.log(e); // some error fn1.
}
}
foo();
複製程式碼
async
4、async/await 的優勢在於處理 then 鏈
單一的 Promise 鏈並不能發現 async/await 的優勢,但是,如果需要處理由多個 Promise 組成的 then 鏈的時候,優勢就能體現出來了(很有意思,Promise 通過 then 鏈來解決多層回撥的問題,現在又用 async/await 來進一步優化它)。
例子一:
假設一個業務,分多個步驟完成,每個步驟都是非同步的,而且依賴於上一個步驟的結果。我們仍然用 setTimeout 來模擬非同步操作:
/**
* 傳入引數 n,表示這個函式執行的時間(毫秒)
* 執行的結果是 n + 200,這個值將用於下一步驟
*/
function takeLongTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(n) {
console.log(`step2 with ${n}`);
return takeLongTime(n);
}
function step3(n) {
console.log(`step3 with ${n}`);
return takeLongTime(n);
}
複製程式碼
- Promise 方式來實現這三個步驟的處理
function doIt() {
console.time("doIt");
const time1 = 300;
step1(time1)
.then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(result => {
console.log(`result is ${result}`);
console.timeEnd("doIt");
});
}
doIt();
複製程式碼
- async/await 方式來實現這三個步驟的處理
async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time2);
const result = await step3(time3);
console.log(`result is ${result}`);
console.timeEnd("doIt");
}
doIt();
複製程式碼
結果和之前的 Promise 實現是一樣的,但是這個程式碼看起來是不是清晰得多,幾乎跟同步程式碼一樣
例子二:
現在把業務要求改一下,仍然是三個步驟,但每一個步驟都需要之前每個步驟的結果。
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(m, n) {
console.log(`step2 with ${m} and ${n}`);
return takeLongTime(m + n);
}
function step3(k, m, n) {
console.log(`step3 with ${k}, ${m} and ${n}`);
return takeLongTime(k + m + n);
}
複製程式碼
- 用 async/await 來寫:
async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time1, time2);
const result = await step3(time1, time2, time3);
console.log(`result is ${result}`);
console.timeEnd("doIt");
}
doIt();
複製程式碼
除了覺得執行時間變長了之外,似乎和之前的示例沒啥區別啊!別急,認真想想如果把它寫成 Promise 方式實現會是什麼樣子?
function doIt() {
console.time("doIt");
const time1 = 300;
step1(time1)
.then(time2 => {
return step2(time1, time2)
.then(time3 => [time1, time2, time3]);
})
.then(times => {
const [time1, time2, time3] = times;
return step3(time1, time2, time3);
})
.then(result => {
console.log(`result is ${result}`);
console.timeEnd("doIt");
});
}
doIt();
複製程式碼
有沒有感覺有點複雜的樣子?那一堆引數處理,就是 Promise 方案的死穴—— 引數傳遞太麻煩了,看著就暈!
5、await in for 迴圈
- await 命令只能用在 async 函式之中,如果用在普通函式,就會報錯。(注意想forEach,map,reduce這種也是函式!!!!)
- 正確的寫法是採用 for 迴圈。!!!!
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 報錯
docs.forEach(function (doc) {
await db.post(doc);
});
}
複製程式碼
上面程式碼會報錯,因為 await 用在普通函式之中了。但是,如果將 forEach 方法的引數改成 async 函式,也有問題。
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 可能得到錯誤結果
docs.forEach(async function (doc) {
await db.post(doc);
});
}
複製程式碼
上面程式碼可能不會正常工作,原因是這時三個 db.post 操作將是併發執行,也就是同時執行,而不是繼發執行。正確的寫法是採用 for 迴圈。
async function dbFuc(db) {
let docs = [{}, {}, {}];
for (let doc of docs) {
await db.post(doc);
}
}
複製程式碼
- 如果確實希望多個請求併發執行,可以使用 Promise.all 方法。
- 先將多個函式(任務)都儲存到doc這個陣列中,就可以儲存多個任務,然後再實現併發執行
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = await Promise.all(promises);
console.log(results);
}
// 或者使用下面的寫法
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 將多個函式(任務)都儲存到doc這個陣列中,就可以儲存多個任務,然後再實現併發執行
let promises = docs.map((doc) => db.post(doc));
let results = [];
for (let promise of promises) {
results.push(await promise);
}
console.log(results);
}
複製程式碼
6、實踐
在實踐中我們遇到非同步場景最多的就是介面請求,那麼這裡就以jquery中的$.get為例簡單展示一下如何配合async/await來解決這個場景。
// 先定義介面請求的方法,由於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) {
// 處理錯誤邏輯
}
}
複製程式碼
7、一個問題測試
題目
可修改下面的 aa() 函式,目的是在一秒後用 console.log() 輸出 want-value
function aa() {
setTimeout(function() {
return "want-value";
}, 1000);
}
複製程式碼
- 但是,有額外要求:
- aa() 函式可以隨意修改,但是不能有 console.log()
- 執行 console.log() 語句裡不能有 setTimeout 包裹
解答
問題的主要目的是考察對非同步呼叫執行結果的處理,既然是非同步呼叫,那麼不可能同步等待非同步結果,結果一定是非同步的
setTimeout() 經常用來模擬非同步操作。最早,非同步是通過回撥來通知(呼叫)處理程式處理結果的
function aa() {
return new Promise((resolve) => {
setTimeout(function() {
resolve('want-value');
},1000);
});
}
async function fn() {
let temp = await aa();
console.log(temp);
}
fn();
複製程式碼
參考文章: