探索javascrip -> async、await

在講async之前,先簡單的提一下promise。
首先,先來糾正一下很多人普遍的錯誤觀點 --> 'promise是異步的', 看代碼:

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是異步的

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)
});

或者再精簡一下

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 函數的改進在於下面四點(這四段是我在別的地方找到的,總結的也很好):

  • 內置執行器。Generator 函數的執行必須依靠執行器,而 Aysnc 函數自帶執行器,調用方式跟普通函數的調用一樣
  • 更好的語義。async 和 await 相較於 * 和 yield 更加語義化
  • 更廣的適用性。co 模塊約定,yield 命令後面只能是 Thunk 函數或 Promise對象。而 async 函數的 await 命令後面則可以是 Promise 或者 原始類型的值(Number,string,boolean,但這時等同於同步操作)
  • 返回值是 Promise。async 函數返回值是 Promise 對象,比 Generator 函數返回的 Iterator 對象方便,可以直接使用 then() 方法進行調用
    (co模塊其實就是將Generator和Promise結合起來,自動執行Generator)

asynch函數也有返回值,是一個promise對象,所以我們可以用.then

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的一個語法糖

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章