因為寫文章時太晚了,有點困,複製錯程式碼了,已改正。
上一篇部落格我們在現實使用和麵試角度講解了Promise(原文可參考《面向面試題和實際使用談promise》),但是Promise 的方式雖然解決了 callback hell,但是這種方式充滿了 Promise的 then()
方法,如果處理流程複雜的話,整段程式碼將充滿 then
,程式碼流程不能很好的表示執行流程。
為什麼是async/await
在es6中,我們可以使用Generator函式控制流程,如下面這段程式碼:
function* foo(x) {
yield x + 1;
yield x + 2;
return x + 3;
}複製程式碼
我們可以根據不斷地呼叫Generator物件的next()
方法來控制函式的流程。但是這樣彷彿不是那麼的語義化。因此,在ES6中封裝了Generator函式的語法糖async函式,但是將其定義在了es7中。ES7定義出的async
函式,終於讓 JavaScript 對於非同步操作有了終極解決方案。Async
函式是 Generator函式的語法糖。使用 關鍵字 Async
來表示,在函式內部使用 await來表示非同步。相較於 Generator,Async函式的改進在於下面幾點:Generator 函式的執行必須依靠執行器,而 Async()
函式自帶執行器,呼叫方式跟普通函式的呼叫一樣。Async
和 await相較於 *
和 yield
更加語義化。async
函式返回值是 Promise 物件,比 Generator函式返回的 Iterator 物件方便,可以直接使用 then()
方法進行呼叫。
那麼,我們通過一段小小的程式碼來說明async/await函式的用法:
未使用async/await的定時函式:
fn = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 2000)
})
}
const Fn = () =>{
fn().then((res) => {
console.log(res)
})
}
Fn()
console.log(2)複製程式碼
我相信能看到這裡的各位程式設計師大佬應該都知道這段程式碼的輸出狀況:先列印2,2s之後列印出1。
使用async/await的定時函式:
fn = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 2000)
})
}
const Fn = async () => {
await fn().then((res) => {
console.log(res)
})
console.log(2)
}
Fn()
複製程式碼
這一段函式的輸出狀況是:2s後列印1,然後列印2。
那麼,why?
我們在字面上理解這兩個單詞async和await:async的意思是非同步,async用於定義一個非同步函式,該函式返回一個Promise。;await的意思是等待,Promise是一個承諾,await也是一個承諾。Promise的承諾是將返回值輸出到then的回掉函式裡面,無論是成功還是失敗。await的承諾是無論颳風還是下雨,我都會等你完成在做其他的步驟。因此,在上面的運用了async/await的程式碼中,會等待fn完全執行完成並且非同步的回撥完成對返回值的處理之後在開始進行下一步操作的。其原理是將非同步函式轉變為同步操作。
實際運用
在上週的工作中,我在一段基於node完成的爬蟲操作中多次運用async/await來控制程式的執行流程:
//虛擬碼
let axiosArr = [];
for (let i = 0, len = arr.length; i < len; i++) {
let params = qs.stringify({
'param': arr[i].index,
})
axiosArr.push(axios.post(url, params, {
headers
}))
}
/*
*上面的迴圈是迴圈抓取2345條資料,平均每個資料要訪問16個介面
*用axios.all同時詢問,當返回結束後將返回值處理
*然後將返回值儲存到mongodb資料庫中
*/
await axios.all(axiosArr).then(
axios.spread(function () {
for (let i = 0, len = arguments.length; i < len; i++) {
let str = `${unescape(arguments[i].data.replace(/\\u/g, '%u'))}`;
str = basics.subStr(basics.deletN(basics.deletS(basics.cutStr(str))));
concentArr[i].concent = str
}
mg.mongodbMain({
name: obj.name,
alias: obj.alias,
type: type,
url: obj.url,
drugsConcent: concentArr
})
}))複製程式碼
其實操作就這麼點,大家看一下程式碼都會懂。但是問題是,當我不使用async/await時,會產生的情況是會先訪問2000+個資料,不斷訪問其16個介面,但是由於promise的then的回撥函式為非同步的,會掛起,而不是直接將資料存到資料庫中。這貌似和我們預想的不一樣啊。因此,我在這裡使用了async/await函式,使用同步處理非同步操作,將promise同步化,當axios.all訪問完成這每一條資料的16個介面後,直接將資料儲存到資料庫中,然後才會走到迴圈的下一層,依舊是訪問下一條資料的16個介面。
async/await的身後事
我們說過了async
函式返回值是 Promise 物件。
const delay = timeout => new Promise(resolve=> setTimeout(resolve, timeout));
async function f(){
await delay(1000);
await delay(2000);
await delay(3000);
return 'done';
}
f().then(v => console.log(v));// 6s之後列印'done'複製程式碼
那麼其內部一旦丟擲異常,則會導致返回的 Promise 物件狀態變為 reject
狀態。丟擲的錯誤而會被 catch
方法回撥函式接收到。
async function e(){
throw new Error('error');
}
e().then(v => console.log(v))
.catch( e => console.log(e));//丟擲的錯誤會被catch捕捉到複製程式碼
並且,async有一個和promise.all相似的特性,就是內部一點有一個await函式報錯,後續的就不再執行了
let fn1 = ()=>{
return new Promise((resolve,reject) => {
setTimeout(()=>{
reject('故意丟擲錯誤');
},500);
});
}
let fn2 = ()=>{
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(1);
},500);
});
}
let getList = async ()=>{
let a = await fn1();
let b = await fn2();
return {first: a,second:b};
}
getList().then(result=> {
console.log(result);
}).catch(err=> {
console.log(err);// 由於fn1的報錯,async的狀態直接變成了rejected
});複製程式碼
當Promise出現的時候,我們彷彿看到了回撥地獄的滅亡。當Async/Await出現時,非同步終於不是一件困難的事情。