• 一文揭秘JavaScript中你不知道的async与await实现原理与细节



    一、回调地狱

    在es6兴起之后许多人都开始使用promise,promise目的是解决es5中的回调地狱(callback hell),那么什么是回调地狱呢?先来提一个需求,现在需要发送n个request请求,第二个请求参数需要第一个请求的结果,第三个请求的参数需要第二个请求的结果,以此类推… ,在没有promise的条件下,我们不难使用callback写出如下的代码:

    function ajax(url, callback) {
        setTimeout(() => {
            callback(Math.random() + url)
        }, 1000);
    }
    
    function request() {
        ajax('url1', (res1 => {
            ajax(`url2?random=${res1}`, (res2) => {
                ajax(`url3?random=${res2}`, (res3) => {
                    ajax(`url4?random=${res3}`, (res4) => {
                        // do something
                    })
                })
            })
        }))
    }
    
    request()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    二、Promise

    这样确实能实现我们的需求,但是这样子的代码有什么缺点呢?不难看出我们的request函数越来越像个三角形 ,代码集中在上部分,下半部分全都是我们的括号,代码阅读性极差! 这时候我们的promise应运而生了,使用promise我们可以这样重构我们的代码如下:

    function ajax(url) {
        return new Promise(resolve => {
            setTimeout(() => {
                resolve(Math.random() + url)
            }, 1000);
        })
    }
    
    function request() {
        ajax('url1').then(res1 => {
            ajax(`url2?random=${res1}`).then((res2) => {
                ajax(`url3?random=${res2}`).then((res3) => {
                    ajax(`url4?random=${res3}`).then((res4) => {
                        // do something
                    })
                })
            })
        })
    }
    
    request()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    肯定有人说,这不还是像个三角形吗?这样使用promise有什么意义呢?此时我们可以借助promise的链式调用重构成下面这样:

    function ajax(url) {
        return new Promise(resolve => {
            setTimeout(() => {
                resolve(Math.random() + url)
            }, 1000);
        })
    }
    
    function request() {
        ajax('url1').then(res1 => {
            return ajax(`url2?random=${res1}`)
        }).then(res2 => {
            return ajax(`url3?random=${res2}`)
        }).then(res3 => {
            return ajax(`url4?random=${res3}`)
        }).then(res4 => {
            // do something
        })
    }
    
    request()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    相对于之前的回调地狱,此时我们的代码是不是比较清晰了。但是!这还不够!这看上去还不够直观。我们想要的是阅读异步代码,类似于阅读同步代码的方式一样方便。

    三、生成器(generator)

    生成器是es6新增的语法,它是一个特殊的迭代器,它可以用来暂停我们函数的执行!这个功能非常强大! 生成器的语法是,在声明函数时在后面增加一个 * 号,那么这个函数就是生成器函数,直接调用该函数得到的是一个生成器句柄,该生成器是不会执行的,必须要调用生成器句柄的next()方法后,生成器才会执行,并且执行到我们的yield处(如果存在yield就执行到第一个yield,不存在则直接执行完毕),该方法的返回值一个对象,结构是 {done: true/false, value: 我们yield后面跟的值} ,如果执行到该生成器函数末尾则 done为true
    关于生成器的知识可以点此处查看更多

    function* foo() {
        console.log('======');
        const a = yield 1;
        console.log('a',a);
    }
    
    const g = foo()
    console.log('11111111')
    const res1 = g.next()
    console.log(res1)
    const res2 = g.next('22222')
    console.log(res2)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    上面代码打印顺序为:

    11111111
    ======
    {done: false, value: 1}
    'a','22222'
    {done: true, value: undefined}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    细心的你一定看出了,我们在next方法中传的参数会赋值给生成器函数中的yield
    左侧,并可以在生成器中拿到这个值后进行使用。

    四、使用生成器同步化promise

    掌握了生成器的知识我们就可以使用生成器来将我们的promise链式调用进行重构如下:

    function ajax(url) {
        return new Promise(resolve => {
            setTimeout(() => {
                resolve(Math.random() + url)
            }, 1000);
        })
    }
    
    function* request() {
        const res1 = yield ajax('url1')
        const res2 = yield ajax(`url2?random=${res1}`)
        const res3 = yield ajax(`url3?random=${res2}`)
        const res4 = yield ajax(`url4?random=${res3}`)
        //    do something
        console.log(res4);
    }
    // 开始调用我们的生成器
    const generator = request();
    generator.next().value.then(res1 => {
        generator.next(res1).value.then(res2 => {
            generator.next(res2).value.then(res3 => {
                generator.next(res3).value.then(res4 => {
                    generator.next(res4)
                })
            })
        })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    可以看到我们的生成器还是三角形,优化一下成链式调用如下:

    generator.next().value.then(res1 => {
        return generator.next(res1).value
    }).then(res2 => {
        return generator.next(res2).value
    }).then(res3 => {
        return generator.next(res3).value
    }).then(res4 => {
        generator.next(res4)
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    此时,我们的主函数已经非常像同步代码了,但是缺点是我们目前必须手动调用该生成器,并且request主函数里面我们不知道有多少次yield调用,因此我们的生成器也不能手动调用多次,这时,我们将该生成器调用代码进行重构,重构成可以自动执行我们的生成器的函数,不需要关心request内部有多少次yield使用,重构如下:

    function ajax(url) {
        return new Promise(resolve => {
            setTimeout(() => {
                resolve(Math.random() + url)
            }, 1000);
        })
    }
    
    function* request() {
        const res1 = yield ajax('url1')
        const res2 = yield ajax(`url2?random=${res1}`)
        const res3 = yield ajax(`url3?random=${res2}`)
        const res4 = yield ajax(`url4?random=${res3}`)
        //    do something
        console.log(res4);
    }
    
    function execGenerator(generatorFn) {
        const generator = generatorFn();
        function exec(res) {
            const { done, value } = generator.next(res)
            if (!done) {
                value.then(exec)
            }
        }
        exec()
    }
    
    execGenerator(request)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    我们增加了一个自动执行函数execGenerator,该函数接受一个生成器参数,并且在内部自动进行递归调用,直至返回值的 done 属性为 true,此时我们的使用方式只需要定义一个request生成器函数,并且执行一下我们的自动执行函数 execGenerator ,我们的request就能像同步代码一样盘跑起来了,并且看起来非常直观。

    五、async、await异步代码究极解决方案

    其实async与await是我们上面生成器的语法糖而已,在内部做的事情跟我们使用生成器做的事情是一样的,使用方式如下:

    function ajax(url) {
        return new Promise(resolve => {
            setTimeout(() => {
                resolve(Math.random() + url)
            }, 1000);
        })
    }
    
    async function request() {
        const res1 = await ajax('url1')
        const res2 = await ajax(`url2?random=${res1}`)
        const res3 = await ajax(`url3?random=${res2}`)
        const res4 = await ajax(`url4?random=${res3}`)
        //    do something
        console.log(res4);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    看起来是不是跟我们的生成器request函数非常类似呢?使用asyncawait可以让我们省去写execGenerator函数的步骤,更加方便了我们的开发!

    至此,我们的async与await已经讲述完毕,欢迎点赞、评论、转发、收藏!

  • 相关阅读:
    Linux C编译器从零开发一
    蓝桥杯 第 1 场算法双周赛 第三题 分组【算法赛】c++ 贪心+双指针
    构建webpack知识体系 | 青训营笔记
    67-94-hive-函数-开窗函数-常用函数-udf自定义函数
    Java浅谈随笔,你都会了吗?
    WPF“x:name”和“name”有什么区别
    轻松掌握JavaScript字符串操作的10个小技巧
    高校档案室建设标准-高校数字档案室建设需考虑哪些因素
    Flutter和iOS混编详解
    简述 HTTP 请求的过程是什么?
  • 原文地址:https://blog.csdn.net/qq_15506981/article/details/126898093