在講async以前,先簡單的提一下promise。
首先,先來糾正一下不少人廣泛的錯誤觀點 --> 'promise是異步的', 看代碼:ajax
console.log(1); let p1 = new Promise(r => { console.log(2); r() }); p1.then(_ => console.log(3)); console.log(4); let p2 = new Promise(r => { console.log(5); r() }); p2.then(_ => console.log(6)); console.log(7); // 打印 1 2 4 5 7 3 6
從打印結果來看,咱們就能夠判定promise是同步的,那麼我就說promise是同步的,then是異步的!也不是,簡單說一下緣由:
先說結果:promise的then也是同步的。這樣輸出的緣由在於,在new promise(fn) fn的r() 函數是異步的會掛起線程,執行到then的時候,then中的代碼塊會立刻開始執行(注意我說地是開始執行),只是把成功的回調函數放到了resovledCallbacks中,可是就算狀態修改完畢爲fulfiled的時候,上面的執行 then(fn)中 fn裏面的代碼執行是會異步操做,也不是當即執行console 由於then的內部實現方式根據promisA規範中也是有一個settimeout 在延時器內部執行aaa的 因此then方法確定同步函數 可是其實表現的永遠都是異步 由於兩個settimeout都保證它是異步去執行成功或失敗的回調函數的,說具體點實際上是r()內部設置了一個延時執行回調,延時setTimeout的最小值,也就是說r纔是異步的,再看數組
console.log(1); let p1 = new Promise(r => { console.log(2); r() }); p1.then(console.log(3)); console.log(4); let p2 = new Promise(r => { console.log(5); r() }); p2.then(console.log(6)); console.log(7); // 打印 1 2 3 4 5 6 7
明白了吧?
對於它所解決的問題主要能夠總結成:promise
promise就再也不多介紹了,有時間你們能夠再深刻研究一下
話說回來,promise真的徹底解決了callback hell嗎?
先來一個場景有四個函數,要求 按順序執行,也就是須要等到前一個promise full以後才能運行,且後一個promise是須要用到上一個promise所返回的值,好比併發
function f1() { return new Promise(resolve => { setTimeout(_ => resolve('f1'), 500) }) } function f2(params) { return new Promise(resolve => { console.log(params); setTimeout(_ => resolve(params + 'f2'), 500) }) } function f3(params) { return new Promise(resolve => { console.log(params); setTimeout(_ => resolve(params + 'f3'), 500) }) } function f4(params) { return new Promise(resolve => { console.log(params); setTimeout(_ => resolve(params + 'f4'), 500) }) }
咱們通常都會這樣寫異步
f1().then(res => { return f2(res) }).then(res => { return f3(res) }).then(res => { return f4(res) });
或者再精簡一下async
f1().then(f2).then(f3).then(f4);
雖然看上去美觀了很多,可是也存在一些問題,好比若是不用第一種方法來寫,用第二種,那麼能夠知道它的可讀性不好,咱們 單看f1().then(f2).then(f3).then(f4);這段代碼實際上是徹底看不出f1,f2,f3,f4到底有什麼聯繫,也更讀不出f2,f3,f4都用了上一層的輸出做爲輸入,最理想的表達我認爲應該這樣函數
f1(); f2(); f3(); f4();
不過,若是這樣那就不能保證咱們的函數是按照順序依次執行了更別說輸入輸出聯繫起來。
這樣,咱們的async登場
因而,你能夠這樣寫學習
void (async function() { let r1 = await f1() let r2 = await f2(r1) let r3 = await f3(r2) await f4(r3) })();
怎樣,是否是簡單明瞭,簡單介紹一下:
ES7 提出的async 函數,終於讓 JavaScript 對於異步操做有了終極解決方案。No more callback hell。
async 函數是 Generator 函數的語法糖。使用 關鍵字 async 來表示,在函數內部使用 await 來表示異步。
想較於 Generator,Async 函數的改進在於下面四點(這四段是我在別的地方找到的,總結的也很好):線程
asynch函數也有返回值,是一個promise對象,因此咱們能夠用.thencode
async function f() { return 1 } console.log(f()) // Promise { 1 }
可是要注意,若是函數執行過程當中遇到了await就會先返回,咱們再看
async function f() { await 2; return 1 } console.log(f()); // Promise { <pending> }
雖然我代碼中reutnr 1可是能夠看到結果倒是返回了一個pending狀態的Promise對象,其中async函數內部return語句返回的值,會成爲then方法回調函數的參數
async function f() { await 2; return 1 } f().then(res => { console.log(res) // 1 })
值得注意的是我一直在強調函數執行,想要表達的就是雖然await是等待執行的意思,可是也並不會對外部產生有反作用的影響
async function f() { await 2; console.log('a') return 1 } f() console.log('b') // b a
從打印結果上咱們看到了雖然程序執行中遇到了await可是它並無阻塞到外部的代碼執行,因此說仍是沒有改變Javascript異步的本質,不過至少咱們能夠在async函數中去很好地控制咱們的流程,咱們來看一道題來瞧瞧這個語法糖的強大之處。
目標 : 發送30次ajax請求,要求30個請求是串行的(即發送請求時必須等待前一個請求res)
這道題若是咱們用常規的promisepromise.then的方法實現起來會有一些難度,咱們先模擬一個ajax請求,假定每一個ajax的timeresponse都是400ms ->
function ajax(n) { return new Promise((rs, rj) => { setTimeout(() => { console.log(n); rs() }, 400) }) }
Promise實現:
let n = 50 let task = ajax(n); function run() { task.then(_ => { --n && (task = ajax(n)) && run() }) } run();
Generator實現
let num = 50; function* Ge() { while (true) { yield ajax(num) } } let re = Ge() function run() { let result = re.next().value result.then(_ => { num-- && run() }) } run()
async實現
let n = 50 async function run() { while (n--) await ajax(n) } run()
作個對比以後一目瞭然
剛纔說async其實就是Generator的語法糖
確定有人會好奇async是怎樣的實現原理,想要理解它,仍是得學習生成器(generator)。畢竟async只是generator的語法糖,跳過它直接學習async固然會錯過不少。async 就等於Generator+自動執行器。
話題回到前邊的例子
void (async function() { let r1 = await f1() let r2 = await f2(r1) let r3 = await f3(r2) await f4(r3) })();
咱們說過async中若是遇到await的話就會等待後邊的Promise返回結果(同步除外),因此上面的代碼中的執行順序是f1->f2->f3,那這樣就帶來一個問題,咱們要向讓f1,2,3同時併發執行怎麼辦?
咱們知道Promise是同步的,當咱們new Promise(...)的時候,事實上是已經開始執行了,只不過返回結果是一個帶狀態的P,那咱們若是想讓f1,2,3並行的話也就有辦法了
void (async function() { let r1 = new Promise(...) let r2 = new Promise(...) let r3 = new Promise(...) await r1 await r2 await r3 })();
這就至關於new Promise中的代碼塊是同時進行的,至於狀態由pending變成full的時間長短由業務需求以及場合來決定,另外一種方法可能會更加直觀一些
void (async function() { let re = await Promise.all([p1, p2, p3]) })();
其中re爲一個數組,值分別對應p1, 2, 3;換作race固然也能夠Promise.race([p1, p2, p3])
若是請求多的話,咱們也可使用map, foreach並行執行
function plist(n) { return new Promise(resolve => { console.log('start:' + n) setTimeout(_ => { resolve(n) }, 2000) }) } let c = [...new Array(100).keys()] let pros = c.map(async n => { return await plist(n) }) for (let p of pros) { p.then(res => console.log('end:' + res)) }
map與forEach 都是並行執行promise數組,但for-in for-of for都是串行的。知道這兩點咱們能夠高效的處理不少異步請求。
最後簡單地說下async的錯誤處理方式
咱們都知道在promise中的異常或者reject都是沒法經過try catch來捕獲,例如
try { Promise.reject('an normal error') } catch (e) { console.log('error comming') console.log(e) }
這個錯誤try catch是捕獲不到的,會報一個UnhandledPromiseRejectionWarning的未捕獲reject的錯誤描述,再好比
function fn() {
try { new Promise(resolve => { JSON.parse(a) resolve() }) } catch (e) { console.log('error comming') console.log(e) } } fn()
這裏直接拋出ReferenceError異常,咱們再把它放在async中
async function fn() { try { await new Promise(resolve => { JSON.parse(a) resolve() }) } catch (e) { console.log('error comming') console.log(e) } } fn()
神奇了,異常居然被捕獲了,其實這個地方我也不是很肯定其真正的緣由,我以爲重點其實就在於await作了什麼,對執行環境產生了什麼影響,先說一下個人觀點 由於promise是非阻塞的也就是說對於promise外部的try,catch來講,內部的promise屬於異步執行,而try cathch是沒法捕獲異步錯誤的, 而await表示等待promise執行,等待這個承諾的執行結果, 並暫停當前async執行環境的代碼執行,也就是說在async下,await咱們甚至能夠認爲它是同步的,阻塞的!因此咱們能夠認爲這個錯誤是同步拋出也就是(await new Promise(...))拋出的,因此會被捕獲。
不過,我卻不建議用這種方式來捕獲async中的異常,一是代碼結構看起來混亂,二是若是try/catch的catch部分有異常,咱們應該如何處理呢?因此我建議用async().catch來處理,由於async無論有沒有返回值,都是返回一個promise對象
async function fn() { } console.log(fn().then) // [Function ...]
而且async也可使用return 來返回一個promise
async function fn() { // return await Promise.resolve(1) // return Promise.resolve(1) }
關於async先簡單介紹到這裏,下一篇文章我會講一下Generator,到時候讓你們知道爲何咱們用 Generator 不多或者爲何說 async是 Generator的一個語法糖