【譯】async 的異步操做模式

原文連接: https://careersjs.com/magazin...

原文做者:Joe Zimmermanjavascript

譯文地址:【譯】async 的異步操做模式 java

基礎筆記的github地址:https://github.com/qiqihaobenben/Front-End-Basics ,能夠watch,也能夠star。

git


我還記得之前執行異步操做須要在愈來愈深的回調地獄中使用回調的那些「好日子」。雖然回調地獄並無徹底成爲過去,可是使用 Promise 來代替回調的嵌套已經顯得簡單多了。es6

我依稀記得 Promise 成爲主流時的那些美好時光,我以前使用 jQuery 的 Deferred,後來 Promise 普及以後,我纔在工做中把 Promise 庫加到咱們的項目中。那時咱們已經有 Babel 了,因此咱們甚至不須要再加一個 Promise 庫。github

不管如何,Promise 在很大程度上實現了它的承諾,使得異步編程更加易於管理。若是關於 Promise 的用法你還不是很熟,能夠在這裏看到更多關於 Promise 的知識。固然,Promise 也有本身的弱點。不少時候,您要麼須要嵌套 Promise ,要麼須要將變量傳遞到外部,由於你須要的一些數據只在 Promise 的 handler 中可用。例如:編程

function getVals () {
    return doSomethingAsync().then(function (val) {
        return doAnotherAsync(val).then(function (anotherVal) {
            // 這裏咱們須要val和anotherVal,因此咱們嵌套了
            return val + anotherVal
        })
    })
}

// 或者...

function getVals () {
    let value

    return doSomethingAsync().then(function (val) {
        // 把 val 賦給最外面的 value,這樣其餘地方都能拿到了
        value = val
        return doAnotherAsync(val)
    }).then(function (anotherVal) {
        // 這裏咱們獲取最外層的 value
        return value + anotherVal
    })
}

這兩個例子自己都很混亂。固然,可使用箭頭函數使他們更有條理數組

function getVals () {
    return doSomethingAsync().then(val => doAnotherAsync(val).then(anotherVal => val + anotherVal))
}

// 或者...

function getVals () {
    let value

    return doSomethingAsync()
    .then(val => (value = val, doAnotherAsync(val)))
    .then(anotherVal => value + anotherVal)
}

上面的代碼可能會清除一些語法上的問題,可是並不能帶來更好的可讀性。好在咱們已經度過了那些「好日子」,如今咱們有了 asyncawait,咱們能夠避免全部的那些廢話。promise

async function getVals () {
    let val = await doSomethingAsync()
    let anotherVal = await doAnotherAsync(val)

    return val + anotherVal
}

這看起來既簡單,又容易理解。我假設你已經對 asyncawait很熟悉了,因此,我不打算介紹太多關於他們的細節。不過,你能夠去 MDN 上覆習更多的 async/await。在這裏,咱們將重點介紹過去在 Promise 中使用的模式,以及這些模式如何轉換爲 async/await異步

「隨機」串行異步操做

在前面的代碼片斷中,咱們在技術上已經討論了一個模式——隨機串行操做——這是咱們首先要討論的。我說的隨機,並非真的隨機。我指的是多個函數,它們可能彼此相關,也可能不相關,但它們是分別調用的。換句話說,隨機操做與在整個輸入列表/輸入數組上執行的操做不一樣。若是你仍然感到困惑,你會在後面的章節中明白我說的意思,當我轉到非隨機操做時,你會看到區別。async

總之,就像我說的,你已經用咱們所說的第一個模式來實現了例子。這些操做是按順序運行的,這意味着第二個操做要等到第一個操做完成後才能啓動。這個模式可能與上面的示例不一樣,在使用 Promise 時,假設咱們不會遇到前面的狀況,即須要將多個值傳遞給後續操做:

function getVals () {
    return doSomethingAsync()
    .then(val => doAnotherAsync(val))
    .then(anotherVal => /* 在這裏,咱們不須要val */ 2 * anotherVal)
}

跟最開始的 Promise 的例子相比,不須要在最終的 handler 中訪問 val ,所以咱們只需鏈式調用 then 便可,而沒必要費心將值傳遞給外部做用域。可是酷炫的是,在 async/await 代碼版本中,咱們除了把上面第一個 async/await 例子中,最後表達式的 val + 換成 2 *外,其餘的什麼都用改:

async function getVals () {
    let val = await doSomethingAsync()
    let anotherVal = await doAnotherAsync(val)

    return 2 * anotherVal
}

這就是 async/await 擅長作的:把一個異步調用行爲模擬成同步的,沒有什麼小把戲,只是簡單的用「先作這個而後作那個」實現代碼。

「隨機」並行異步操做

好了,此次咱們看一下並行運行的操做,這些操做都不關心其餘操做是否已經完成,也不依賴於其餘操做產生的 value。當用 Promise 時,能夠這樣寫(忽略這樣一個事實:我重用了以前的異步函數名 getVals ,可是它們的使用方式徹底不一樣;它們的函數名已經很明顯表示它們是異步的;它們不必定是前面示例中使用的函數):

function getVals () {
    return Promise.all([doSomethingAsync(), doAnotherAsync()])
    .then(function ([val, anotherVal]) {
        return val + anotherVal
    })
}

咱們使用 Promise.all 是由於它容許咱們傳遞任意數量的 Promise,而且會一塊兒執行完,經過一個 then 函數把全部結果返回給咱們。Promise 還有其餘方法,例如 Promise.anyPromise.some 等,究竟選哪一個,這取決於你是否使用 Promise 庫或某些 Babel 插件,固然還取決於你的用例以及你要如何處理輸出或被拒絕的可能(reject)。在任何狀況下,模式都很是類似,你只需選擇一個不一樣的 Promise 方法,就會獲得不一樣的結果。

async/await 不容許咱們脫離 Promise.all 或其組成部分來使用是把雙刃劍。很差的是,async/await 在後臺隱藏了對 Promise 的使用,可是咱們須要顯式地使用 Promise 才能並行執行操做。好的一面是,這意味着咱們不用學習任何新東西,咱們只要在之前使用的基礎上刪掉傳遞給 then 回調的額外參數就行。而後,咱們使用 await 僞裝咱們的並行操做都是瞬間完成。

async function getVals () {
    let [val, anotherVal] = await Promise.all([doSomethingAsync(), doAnotherAsync()])
    return val + anotherVal
}

所以,async/await 不只僅是刪除回調和沒必要要的嵌套,更重要的是,它使異步編程模式看起來更像同步編程模式,這樣代碼對開發人員就會更友好。

迭代並行異步操做

這裏的操做不是「隨機」的了。在這裏,咱們對一組值進行迭代,並對每一個值執行相同的異步操做。在這個並行版本中,每一個元素都是同時處理的。

Promise 實現以下:

function doAsyncToAll (values /* array */) {
    return Promise.all(values.map(doSomethingAsync))
}

就這麼簡單, 用 async/await 應該怎麼作呢?其實你什麼都不用作。非要作點什麼的話,只會讓代碼更冗長。

async function doAsyncToAll (values /* array */) {
    return await Promise.all(values.map(doSomethingAsync))
}

看上去除了添加幾個僞裝使你看起來很聰明並使用現代 JavaScript 的關鍵字外,其餘的毛用沒有。但實際上,你添加這幾個關鍵字沒有任何價值,反而還會致使 JavaScript 引擎可能會運行得更慢。可是,若是你的代碼更復雜一些, async/await 確定能夠提供一些好處:

function doAsyncToAll (values /* array */) {
    return Promise.all(values.map(val => {
        return doSomethingAsync(val)
        .then(anotherVal => doAnotherAsync(anotherValue * 2))
    }))
}

雖然上面的代碼看着也還行,可是 async/await 更簡潔條理。

function doAsyncToAll (values /* array */) {
    return Promise.all(values.map(async val => {
        let anotherVal = await doSomethingAsync(val)
        return doAnotherAsync(anotherValue * 2)
    }))
}

就我我的而言,我認爲這很清楚,至少從回調內部能夠進行映射,可是這裏有些人可能會感到困惑。當我第一次開始使用 async/await 時,我在回調中看到await,這讓我認爲這些回調沒有並行觸發。這是人們在嵌套函數中使用 async/await 時常常會犯的一個錯誤,而且是與直接使用 Promise 相比, async/await 顯得可能不那麼容易理解的實例。可是,當你使用嵌套異步函數時,稍微暴露一下能夠幫助你更容易地發現問題,所以它們的內部函數與外部函數是分離的,而且 await 不會暫停外部函數。

緊接上文,一旦在你的函數中增長更多的步驟,閱讀 Promise 的複雜度也會上升,使用 async/await 就會顯得更有效果。

function doAsyncToAll (values /* array */) {
    return Promise.all(values.map(val => {
        return doSomethingAsync(val)
        .then(anotherVal => doAnotherAsync(anotherValue * 2))
    }))
    .then(newValues => newValues.join(','))
}

這些不一樣層級的 then 調用真的會讓一我的的頭腦混亂,因此讓咱們用更現代的方式來實現它:

async function doAsyncToAll (values /* array */) {
    const newValues = await Promise.all(values.map(async val => {
        let anotherVal = await doSomethingAsync(val)
        return doAnotherAsync(anotherValue * 2)
    }))
    return newValues.join(',')
}

以往來看,還有其餘方法能夠解決這種狀況,但解決並非最終目標:可讀性和可維護性纔是最重要的,一般這是 async/await 最方便的地方。編寫一般也更簡單,由於咱們就是按照之前那種同步來寫的。

迭代串行異步操做

咱們回到最後的模式。咱們再次遍歷一個列表,並對列表中的每項進行異步操做,可是此次,咱們同一時間只執行一個操做。換句話說,在咱們完成對第一項的操做以前,不能對第二項進行任何操做。

function doAsyncToAllSequentially (values) {
    return values.reduce((previousOperation, val) => {
        return previousOperation.then(() => doSomethingAsync(val))
    }, Promise.resolve())
}

爲了按照順序執行,咱們須要將 then 的調用鏈起來,前一個操做生成後一個操做。這能夠經過reduce實現,也是最合理的方式。請注意,你須要傳遞一個 resolved 的 Promise 做爲最後一個 reduce 的參數,這樣第一次迭代就能夠觸發後面一連串的調用了。

在這裏,咱們將再次看到 async/await 耀眼的光芒,咱們不須要像是 reduce 這樣的任何數組方法,只須要一個普通的循環,而後在循環中使用 await

async function doAsyncToAllSequentially (values) {
    for (let val of values) {
        await doSomethingAsync(val)
    }
}

若是你使用 reduce 的緣由不只僅是爲了串行操做,那麼你仍然能夠繼續使用。例如,若是你打算把全部操做的結果相加

function doAsyncToAllSequentially (values) {
    return values.reduce((previousOperation, val) => {
        return previousOperation.then(
            total => doSomethingAsync(val).then(
                newVal => total + newVal
            )
        )
    }, Promise.resolve(0))
}

上面的代碼只會讓我頭腦混亂。使人驚訝的是,即便使用了 Promise ,咱們也沒有防止回調地獄的重現。即便咱們使用了箭頭函數,咱們能夠老是在一行內寫完代碼,可是這並無讓代碼變得好理解。可是,使用 async/await 就可讓代碼更簡潔條理:

async function doAsyncToAllSequentially (values) {
    let total = 0
    for (let val of values) {
        let newVal = await doSomethingAsync(val)
        total += newVal
    }
    return total
}

若是使用 async/await 時,你仍然喜歡在將數組單個值合併時使用 reduce ,那也能夠參照下面的代碼 :

async function doAsyncToAllSequentially (values) {
    return values.reduce(async (previous, val) => {
        let total = await previous
        let newVal = await doSomethingAsync(val)

        return total + newVal
    }, Promise.resolve(0))
}

總結

在編寫異步代碼時,相對較新的 async/await 關鍵字確實改變了編碼體驗。它們幫助咱們消除或減弱了長期困擾 JavaScript 開發人員的異步代碼編寫和閱讀方面的問題:回調地獄。讓咱們可以以一種更容易理解的方式讀寫異步代碼。所以,瞭解如何有效地使用這項新技術是很重要的,我但願以上這些模式對你有所幫助。

相關文章
相關標籤/搜索