async/await 異步應用的經常使用場景

前言

async/await 語法用看起來像寫同步代碼的方式來優雅地處理異步操做,可是咱們也要明白一點,異步操做原本帶有複雜性,像寫同步代碼的方式並不能下降本質上的複雜性,因此在處理上咱們要更加謹慎, 稍有不慎就可能寫出不是預期執行的代碼,從而影響執行效率。下面將簡單地描述一下一些平常經常使用場景,加深對 async/await 認識
最廣泛的異步操做就是請求,咱們也能夠用 setTimeOut 來簡單模擬異步請求。html

場景1. 一個請求接着一個請求

相信這個場景是最常遇到,後一個請求依賴前一個請求,下面以爬取一個網頁內的圖片爲例子進行描述,使用了 superagent 請求模塊, cheerio 頁面分析模塊,圖片的地址須要分析網頁內容得出,因此必須按順序進行請求。數組

const request = require('superagent')
const cheerio = require('cheerio')
// 簡單封裝下請求,其餘的相似

function getHTML(url) {
// 一些操做,好比設置一下請求頭信息
return superagent.get(url).set('referer', referer).set('user-agent', userAgent)
}
// 下面就請求一張圖片
async function imageCrawler(url) {
    let res = await getHTML(url)
    let html = res.text
    let $ = cheerio.load(html)
    let $img = $(selector)[0]
    let href = $img.attribs.src
    res = await getImage(href)
    retrun res.body
}
async function handler(url) {
    let img = await imageCrawler(url)
    console.log(img) // buffer 格式的數據
    // 處理圖片
}
handler(url)

上面就是一個簡單的獲取圖片數據的場景,圖片數據是加載進內存中,若是隻是簡單的存儲數據,能夠用流的形式進行存儲,以防止消耗太多內存。
其中 await getHTML 是必須的,若是省略了 await 程序就不能按預期獲得結果。執行流程會先執行 await 後面的表達式,其實際返回的是一個處於 pending 狀態的 promise,等到這個 promise 處於已決議狀態後纔會執行 await 後面的操做,其中的代碼執行會跳出 async 函數,繼續執行函數外面的其餘代碼,因此並不會阻塞後續代碼的執行。promise

場景2.併發請求

有的時候咱們並不須要等待一個請求回來才發出另外一個請求,這樣效率是很低的,因此這個時候就須要併發執行請求任務。下面以一個查詢爲例,先獲取一我的的學校地址和家庭住址,再由這些信息獲取詳細的我的信息,學校地址和家庭住址是沒有依賴關係的,後面的獲取我的信息依賴於二者併發

async function infoCrawler(url, name) {
        let [schoolAdr, homeAdr] = await Promise.all([getSchoolAdr(name), getHomeAdr(name)])
        let info = await getInfo(url + `?schoolAdr=${schoolAdr}&homeAdr=${homeAdr}`)
        return info
    }

上面使用的 Promise.all 裏面的異步請求都會併發執行,並等到數據都準備後返回相應的按數據順序返回的數組,這裏最後處理獲取信息的時間,由併發請求中最慢的請求決定,例如 getSchoolAdr 遲遲不返回數據,那麼後續操做只能等待,就算 getHomeAdr 已經提早返回了,固然以上場景必須是這麼作,可是有的時候咱們並不須要這麼作。
上面第一個場景中,咱們只獲取到一張圖片,可是可能一個網頁中不止一張圖片,若是咱們要把這些圖片存儲起來,實際上是沒有必要等待圖片都併發請求回來後再處理,哪張圖片早回來就存儲哪張就好了異步

let imageUrls = ['href1', 'href2', 'href3']
async function saveImages(imageUrls) {
    await Promise.all(imageUrls.map(async imageUrl => {
    let img = await getImage(imageUrl)
    return await saveImage(img)
}))
    console.log('done')
}

// 若是咱們連存儲是否所有完成也不關心,也能夠這麼寫async

let imageUrls = ['href1', 'href2', 'href3']
// saveImages() 連 async 都省了
function saveImages(imageUrls) {
    imageUrls.forEach(async imageUrl => {
    let img = await getImage(imageUrl)
    saveImage(img)
    })
}

可能有人會疑問 forEach 不是不能用於異步嗎,這個說法我也在剛接觸這個語法的時候就據說過,很明顯 forEach 是能夠處理異步的,只是是併發處理,map 也是併發處理,這個怎麼用主要看你的實際場景,還要看你是否對結果感興趣函數

場景3.錯誤處理

一個請求發出,能夠會遇到各類問題,咱們是沒法保證必定成功的,報錯是常有的事,因此處理錯誤有時頗有必要, async/await 處理錯誤也很是直觀, 使用 try/catch 直接捕獲就能夠了網站

async function imageCrawler(url) {
    try {
        let img = await getImage(url)
        return img
    } catch (error) {
        console.log(error)
    }
}

// imageCrawler 返回的是一個 promise 能夠這樣處理ui

async function imageCrawler(url) {
    let img = await getImage(url)
    return img
}
imageCrawler(url).catch(err => {
    console.log(err)
})

可能有人會有疑問,是否是要在每一個請求中都 try/catch 一下,這個其實你在最外層 catch 一下就能夠了,一些基於中間件的設計就喜歡在最外層捕獲錯誤url

async function ctx(next) {
    try {
        await next()
    } catch (error) {
        console.log(error)
    }
}

場景4. 超時處理

一個請求發出,咱們是沒法肯定何時返回的,也總不能一直傻傻的等,設置超時處理有時是頗有必要的

function timeOut(delay) {

return new Promise((resolve, reject) => {
    setTimeout(() => {
    reject(new Error('不用等了,別傻了'))
    }, delay)
})

}

async function imageCrawler(url,delay) {

try {
    let img = await Promise.race([getImage(url), timeOut(delay)])
    return img
} catch (error) {
    console.log(error)
}

}
這裏使用 Promise.race 處理超時,要注意的是,若是超時了,請求仍是沒有終止的,只是再也不進行後續處理。固然也不用擔憂,後續處理會報錯而致使從新處理出錯信息, 由於 promise 的狀態一經改變是不會再改變的

場景5. 併發限制

在併發請求的場景中,若是須要大量併發,必需要進行併發限制,否則會被網站屏蔽或者形成進程崩潰

async function getImages(urls, limit) {
    let running = 0
    let r
    let p = new Promise((resolve, reject) => {
    r = resolve
    })
    function run() {
        if (running < limit && urls.length > 0) {
            running++
            let url = urls.shift();
            (async () => {
                let img = await getImage(url)
                running--
                console.log(img)
                if (urls.length === 0 && running === 0) {
                    console.log('done')
                    return r('done')
                } else {
                    run()
                }
            })()
            run()  // 當即到併發上限
        }
    }
    run()
    return await p
}

總結

以上列舉了一些平常場景處理的代碼片斷,在遇到比較複雜場景時,能夠結合以上的場景進行組合使用,若是場景過於複雜,最好的辦法是使用相關的異步代碼控制庫。若是想更好地瞭解 async/await 能夠先去了解 promise 和 generator, async/await 基本上是 generator 函數的語法糖,下面簡單的描述了一下內部的原理。

function delay(time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(time)
        }, time)
    })
}
function *createTime() {
    let time1 = yield delay(1000)
    let time2 = yield delay(2000)
    let time3 = yield delay(3000)
    console.log(time1, time2, time3)
}
let iterator = createTime()
console.log(iterator.next())
console.log(iterator.next(1000))
console.log(iterator.next(2000))
console.log(iterator.next(3000))
// 輸出

{ value: Promise { <pending> }, done: false } 

{ value: Promise { <pending> }, done: false }

 { value: Promise { <pending> }, done: false } 

1000 2000 3000 

{ value: undefined, done: true }

能夠看出每一個 value 都是 Promise,而且經過手動傳入參數到 next 就能夠設置生成器內部的值,這裏是手動傳入,我只要寫一個遞歸函數讓其自動添進去就能夠了

function run(createTime) {
    let iterator = createTime()
    let result = iterator.next()
    function autoRun() {
        if (!result.done) {
            Promise.resolve(result.value).then(time => {
            result = iterator.next(time)
            autoRun()
        }).catch(err => {
            result = iterator.throw(err)
            autoRun()
            })
        }
    }
    autoRun()
}
run(createTime)

promise.resove 保證返回的是一個 promise 對象 可迭代對象除了有 next 方法還有 throw 方法用於往生成器內部傳入錯誤,只要生成內部能捕獲該對象,生成器就能夠繼承運行,相似下面的代碼

function delay(time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (time == 2000) {
            reject('2000錯誤')
        }
            resolve(time)
        }, time)
    })
}
function *createTime() {
    let time1 = yield delay(1000)
    let time2
    try {
        time2 = yield delay(2000)
    } catch (error) {
        time2 = error
    }
    let time3 = yield delay(3000)
    console.log(time1, time2, time3)
}

能夠看出生成器函數其實和 async/await 語法長得很像,只要改一下 async/await 代碼片斷就是生成器函數了

async function createTime() {
    let time1 = await delay(1000)
    let time2
    try {
        time2 = await delay(2000)
    } catch (error) {
        time2 = error
    }
    let time3 = await delay(3000)
    console.log(time1, time2, time3)
}

function transform(async) {
  let str = async.toString()
  str = str.replace(/async\s+(function)\s+/, '$1 *').replace(/await/g, 'yield')
  return str
}
相關文章
相關標籤/搜索