上一篇博客我們在現實使用和麵試角度講解了Promise(原文可參考《面向面試題和實際使用談promise》),但是Promise 的方式雖然解決了 callback hell,但是這種方式充滿了 Promise的 then() 方法,如果處理流程複雜的話,整段代碼將充滿 then,代碼流程不能很好的 ...
上一篇博客我們在現實使用和麵試角度講解了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) }) } Fn() console.log(2)
這一段函數的輸出狀況是: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出現時,非同步終於不是一件困難的事情。