从 生成器 到 promise+async

请叫我海龟先生
• 阅读 1698

本文主要讲解js中关于生成器的相关概念和作用,以及到后面结合 promise 实现 es7中的 async 原理,你将学习到js中异步流程控制相关知识

1、认识生成器

思考如下代码:

        let x = 1
        function foo() {
            x++
            bar()
            console.log(x) // 3
        }
        function bar() {
            x++
        }
        foo()

如上代码,我们确定知道,运行foo函数时,bar函数一定也会在 x++后执行,于是得到 x = 3,那么有没有其他方式,能不能在 foo函数 x++后,先暂停一下,外部在执行下 bar 函数,然后再往下执行呢?(这其实是属于抢占线程的方式,js 并不支持)如下代码:

        let x = 1
        function *foo() {
            x++
            console.log('第一次next执行到 yield处,暂停') 
            yield
            console.log('第二次执行next')
            console.log(x) // 3
        }
        function bar() {
            x++
        }
        let it = foo()
        it.next()
        bar()
        it.next()

解释:

上面代码新增了几个可能没有见过的标志符 一个*号yield*号的作用是标识foo函数是一个生成器,当生成器函数执行时,遇到内部的 yield便会暂停函数执行,而是等待下一次的 恢复执行(next)

代码释义:

// 并不会真的执行foo函数,而是构造 foo 函数的迭代器 iterator (it)
let it = foo()
// 这是第一次执行,内部会执行到 yield 便停止,等待下一次 next 再恢复执行
it.next()
在这期间便执行其他代码 bar()
// 第二次执行,从上一次 yield 处恢复执行
it.next()

上面的代码我们可以知道,生成器函数,可以实现让函数暂停和恢复执行,生成器函数先构造迭代器,然后通过迭代器上的 next 方法控制函数的执行

2、简单补充下迭代器

迭代器是一个定义良好的接口,用于从一个生产者一步步得到一系列值。(引用于 《你不知道的javascript中卷》),从上面例子中, 不断调用next方法来恢复函数执行应该能领悟到 一步步得到一系列值 这句话的意思。

迭代器会返回一个对象 {value: xxx, done: false} ,值 value 和当前迭代状态 done,会依据这个状态来判定是否结束迭代,es6 新增了 Symbol.iterator 来调用迭代,实际上部分 js 数据已经内置了 迭代方法,比如数组

var a = [1,3,5,7,9];
for (var v of a) {
 console.log( v );
}
// 1 3 5 7 9 
for of 其实就自动使用迭代来完成的,
而且在面对数组这样有限的集合时,最后会自动的 返回 done true 来停止迭代
同时我们也可以使用 break 或 return 的方式来结束迭代

3、深入一点 生成器

上面我们只是简单了解了生成器和迭代相关的知识,接下来我们稍微深入了解下生成器 思考如下代码:

        var z = 1
        function *foo() {
            var x = yield 2
            z++
            var y = yield(x * z)
            console.log(x,y,z)
        }
        var it = foo()
        var y1 =  it.next()
        var y2 =  it.next(6)
        var y3 =  it.next(8)
        有点复杂了,接下来来分析下

上面的代码比刚刚的栗子复杂多了,不仅仅是简单控制程序的启动和暂停,而是多了消息的传递(输入,输出) yield 可以向外部传递值,可以理解为return ,同时也可以接收由外部 next( xxx ) 传递过来的值。

代码释义:

        // 生成 foo 函数的迭代器
        var it = foo()
        / *第一次,执行到第一个 yield 处,
          *此时 yield 2,表示向外界输出了一个2,
          *则 y1 = {value: 2, done: false}
        */
        var y1 =  it.next()
        /**
         *第二次,从第一个 yield 处恢复执行,同时 next(6),向内部输入 6
         * x = 6 z++ ---> 2
         * 遇到 yield(x * z) 停止,并且向外返回 6*2 = 12
         * y2 = {value: 12, done: false}
        **/
        var y2 =  it.next(6)
        /**
         * 第三次,next(8) 则 内部 y = 8
        **/
        var y3 =  it.next(8)
        所以最终 console.log(x,y,z)  6 8 2

总结下:

  1. 当第一次调用生成器 var it = foo() 会构造一个迭代器,但不会真的执行
  2. 第一次调用 it.next()时, 函数会执行到第一个 yield处,并将yield 后的值返回(相当于return),(注意:第一次调用的next() 传递的参数是无效的)
  3. 第二次调用next(xx),可以传递参数,参数将赋值到 第一次 yield 处,函数继续执行到 下一个yeild处停止,(下一个yield 也可以返回值),然后一直这样进行下去
  4. it.next() 都会返回一个对象 { value: xxx,done: boole },done 用来标识迭代是否结束,当没有返回值时 value 就是 undefined

4、生成器和异步

上面我们大体了解清楚了生成器的使用,现在我们来看看用生成器来控制异步

思考如下代码:

        // 模拟一个 ajax 的请求函数
        function ajax(x,y,time = 3000) {
             setTimeout(() => {
                it.next( x + y )
             },time)
         }
         function *mian() {
             try {
                let data = yield ajax(1,2)
                console.log('ajax 结果',data)
             } catch (error) {
                 console.log('错误',error)
             }

         }
         let it = mian()
         it.next()

在之前,可能会考虑使用回调的方式去解决这样的异步问题,现在我们可以通过生成器去控制,其实就是当异步结束的时候,通过构造的迭代器去调用,从而实现异步的控制。

因为 yield阻塞 mian 函数的执行,所以当等到异步结束时,再通过 it.next( x + y )重新启动 mian 函数,从而完成异步的控制。

生成器也可以抛出错误,(it.throw)也可以通过 throw 手动输出错误,使得我们可以使用 try catch 来捕捉同步错误。

5、生成器 和 Promise

    之前写请求可能是这样的
    function foo(x,y) {
         return request(
         "http://some.url.1/?x=" + x + "&y=" + y
         );
    }
    foo( 11, 31 ).then(
         function(text){
             console.log( text );
         },
         function(err){
             console.error( err );
         }
    );

foo 会返回一个 Promise,使得我们再去操作,当搭配 生成器时

    function *main() {
         try {
              // 返回一个 Promise
             var text = yield foo( 11, 31 );
             console.log( text );
         }
         catch (err) {
             console.error( err );
         }
    } 
    var it = main()
    var p = it.next().value
    // Promise 有结果后,next 传入 结果值 
    p.then(
         function(text){
             it.next( text );
         },
         function(err){
             it.throw( err );
         }
    ); 

上面的代码可以做知道,我们可以直接 yield 一个 Promise,当Promise决议后,再输入结果值,在 es7之前,会通过一些 库取实现这样的封装,es7后新增了 async await 这样的语法

    async function main() {
             try {
                 var text = await foo( 11, 31 );
                 console.log( text );
             }
             catch (err) {
                 console.error( err );
             }
    } 

上面代码没有了 ,也不需要 yield 出 Promise,而是使用 await 等待他的决议。 *如果你 await 了一个 Promise,async 函数就会自动获知要做什么,它会暂停这个函数(就像生成器一样),直到 Promise 决议。我们并没有在这段代码中展示这一点,但是调用一个像 main() 这样的 async 函数会自动返回一个 promise。在函数完全结束之后,这个 promise会决议**。(引用于原文)

6、实现一个 async await

        // 异步请求
        function ajax(x,y,time = 3000) {
            return new Promise((resolve) => {
                setTimeout(() => {
                    resolve(x + y)
                },time)
            })
         }
        // 其实结构和 async await 就很类似了
        function *mian() {
                    try {
                        let data = yield ajax(5,6)
                        return data
                    } catch (error) {
                        console.log('错误', error)
                    }
        }

        run(mian).then((res) => {
            console.log(res,'res')
        })
        /**
        * run 函数内部其实就是返回promise,循环调用生成器的方式来实现的
        **/
        function run(gen) {
            var args = [].slice.call(arguments, 1), it;
            // 在当前上下文中初始化生成器
            it = gen.apply(this, args);
            // 返回一个promise用于生成器完成
            return Promise.resolve().then(function handleNext(value) {
                // 对下一个yield出的值运行
                var next = it.next(value);
                return (function handleResult(next) {
                    // 生成器运行完毕了吗?
                    if (next.done) {
                        return next.value;
                    }
                    // 否则继续运行
                    else {
                        return Promise.resolve(next.value).then(
                            // 成功就恢复异步循环,把决议的值发回生成器
                            handleNext,
                            // 如果value是被拒绝的 promise,
                            // 就把错误传回生成器进行出错处理
                            function handleErr(err) {
                                return Promise.resolve(it.throw(err)).then(handleResult);
                            }
                        );
                    }
                })(next);
            });
        }

个人理解: async 和 await 其实就是于、把之前想要结合 生成器+promise 的方式包装了下,当async 标志的函数,内部 await 一个promise,内部就会暂停执行,当 promise有结果后(可以理解为有结果就 next( data ) 恢复函数执行),就将结果值返回了

想起以前,总有面试官问,你知道 async await 吗?我说,async 不就是一个标识符吗,表明这是一个异步函数,会自动将return 的值封装成 promise,当内部遇到 await 便会暂停函数执行,或许面试官想听的是 关于生成器方面的吧,如果有更好的回答,或者解释,不妨留言探讨。

7、后续补充

在前面我们知道了 async其实就是 Generator函数的语法糖,把 * 号换成了 async,把 yield 换成了 await,那么,这样有什么好处或者区别呢?

  1. async修饰符,使得和其他函数调用无差异,不需要调用next()方法,同时直接返回一个promise
  2. async和await语义更好,不像*和yield,同时 yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。

对比下两种方式,就能体会到差异了

点赞
收藏
评论区
推荐文章
秃头王路飞 秃头王路飞
4个月前
webpack5手撸vue2脚手架
webpack5手撸vue相信工作个12年的小伙伴们在面试的时候多多少少怕被问到关于webpack方面的知识,本菜鸟最近闲来无事,就尝试了手撸了下vue2的脚手架,第一次发帖实在是没有经验,望海涵。languageJavaScript"name":"vuecliversion2","version":"1.0.0","desc
Jacquelyn38 Jacquelyn38
1年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
blmius blmius
1年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
小森森 小森森
4个月前
校园表白墙微信小程序V1.0 SayLove -基于微信云开发-一键快速搭建,开箱即用
后续会继续更新,敬请期待2.0全新版本欢迎添加左边的微信一起探讨!项目地址:(https://www.aliyun.com/activity/daily/bestoffer?userCodesskuuw5n)\2.Bug修复更新日历2.情侣脸功能大家不要使用了,现在阿里云的接口已经要收费了(土豪请随意),\\和注意
Easter79 Easter79
1年前
swap空间的增减方法
(1)增大swap空间去激活swap交换区:swapoff v /dev/vg00/lvswap扩展交换lv:lvextend L 10G /dev/vg00/lvswap重新生成swap交换区:mkswap /dev/vg00/lvswap激活新生成的交换区:swapon v /dev/vg00/lvswap
Stella981 Stella981
1年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Stella981 Stella981
1年前
Python之time模块的时间戳、时间字符串格式化与转换
Python处理时间和时间戳的内置模块就有time,和datetime两个,本文先说time模块。关于时间戳的几个概念时间戳,根据1970年1月1日00:00:00开始按秒计算的偏移量。时间元组(struct_time),包含9个元素。 time.struct_time(tm_y
Wesley13 Wesley13
1年前
MySQL查询按照指定规则排序
1.按照指定(单个)字段排序selectfromtable_nameorderiddesc;2.按照指定(多个)字段排序selectfromtable_nameorderiddesc,statusdesc;3.按照指定字段和规则排序selec
Wesley13 Wesley13
1年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
helloworld_34035044 helloworld_34035044
6个月前
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为