async-await 实现原理

2,766 阅读4分钟

前言

阅读该文章之前,你需要对async-await和generator有一定的了解。如果不了解的话,可以查看之前的文章async-await的用法以及生成器-迭代器

提出需求

当前有这么一个网络请求的函数,我想要做到后一次网络请求传递的参数是基于前一次网络请求的结果。例如:第一次传递参数为'aaa',然后第二次在第一次请求结果的基础上加上'bbb',以此类推。

function request(url) {
    return new Promise((resolve, reject) => {
        setTimeout(() =>{  // 用定时器模拟网络请求
            resolve(url)
        }, 1000)
    })
}

解决方法

Promise实现

request('aaa').then(res => {
    // 这里的res为'aaa'
    request(res + 'bbb').then(res => {
        // 这里的res为'aaabbb'
        request(res + 'ccc').then(res => {
            console.log(res)  // 'aaabbbccc'
        })
    })
})
  • 可以看到上面使用Promise.then的方法是可以实习我们想要的效果的,在then中继续进行request从而达到我们想要实现的效果。可是上面的代码也出现了多层then的嵌套,当前只是发送3次网络请求,如果是30次,那么这样的代码会形成回调地狱。

async-await实现

async function foo() {
    const result1 = await request('aaa')  // result1结果是'aaa'
    const result2 = await request(result1 + 'bbb') // result2结果是'aaabbb'
    const result3 = await request(result2 + 'ccc')
    console.log(result3)  // 'aaabbbccc'
}

foo()
  • 可以看到使用了async和await同样可以实现想要的效果,而且使用该方法让我们可以使用同步的方法进行代码的书写。这对我们阅读代码是非常友好的。
  • 这也是推荐的解决该需求的办法。

使用generator实现

// 定义生成器函数
function* generator() {
    const result1 = yield request('aaa') // 第一次调用next会停止在这里
    const result2 = yield request(result1 + 'bbb') // 第二次调用next会停止在这里
    const result3 = yield request(result2 + 'ccc') // 第三次调用next会停止咋这里
    // 第四次调用next,执行之后的所有代码
    console.log(result3) // 打印 'aaabbbccc'
}

const gene = generator() // 调用生成器函数生成生成器
// 调用next会返回一个迭代器,该迭代器的value就是request('aaa')返回的Promise对象
gene.next().value.then(res1 => {
    // res1结果为'aaa'
    // 调用next函数并将res1传递进去,这个res1就会传递给generator函数里的result1
    // 这里的next函数会返回一个迭代器,该迭代器的value就是request(result1 + 'bbb')返回的Promise对象
    gene.next(res1).value.then(res2 => {
        // res2结果为'aaabbb'
        // 调用next函数并将res2传递进去,这个res2就会传递给generator函数里的result2
        // 这里的next函数会返回一个迭代器,该迭代器的value就是request(result2 + 'ccc')返回的Promise对象
        gene.next(res2).value.then(res3 => {
            // res3结果为'aaabbbccc'
            // 调用next函数并将res3传递进去,这个res3就会传递给generator函数里的result3
            gene.next(res3)
        })
    })
})
  • 看到上面的代码可能会感觉有点难,而且看起来比用Promise实现的回调更加严重。
  • 但是如果我们单纯看generator函数,会发现generator函数里的写法和使用async-await的写法是基本一致的,只是关键字不同而已。
  • 观察下面的对next函数的操作,我们可以发现其实是有规律的,那么我们可以使用一个递归函数来帮助我们实现该过程,而不需要我们手动进行编写。

实现自动执行next操作的函数

function execuGene(generatorFn) {
    // 执行传递进来的生成器函数,得到生成器
    const gene = generatorFn()
    // 定义递归函数
    function recurse(url) {
        // 调用next并传递url参数,得到迭代器
        const result = gene.next(url)
        // 判断迭代器的done的状态
        if (!result.done) {
            // 进入这里,表示生成器未结束
            result.value.then(res =>{
                // 递归调用recurse
                recurse(res)
            })
        }
    }
    
    recurse()
}

function* generator() {
    const result1 = yield request('aaa') // 第一次调用next会停止在这里
    const result2 = yield request(result1 + 'bbb') // 第二次调用next会停止在这里
    const result3 = yield request(result2 + 'ccc') // 第三次调用next会停止咋这里
    // 第四次调用next,执行之后的所有代码
    console.log(result3) // 打印 'aaabbbccc'
}
// 现在只需要将上面执行的一大推next的代码改成这一行代码即可完成和上面相同的效果
execuGene(generator)
  • 可以看到经过一个处理函数,我们现在编写的代码已经和使用async-await实现的基本上没有什么区别了。而这也就是async-await的实现原理

总结

async-await本质上就是generator和Promise的语法糖。通过generator函数yield关键字实现对函数的精准控制,做到通过同步代码实现异步操作。而对于对生成器的next调用的繁琐操作,我们也可以通过一个递归函数来帮助我们完成