【译】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 开发人员的异步代码编写和阅读方面的问题:回调地狱。让咱们可以以一种更容易理解的方式读写异步代码。所以,了解如何有效地使用这项新技术是很重要的,我但愿以上这些模式对你有所帮助。

相关文章
相关标签/搜索