当前位置: 首页 > news >正文

从生成器函数Generator出发,聊聊Async await

简介

整篇文章大概会涉及到以下内容:

  1. 生成器函数的概念、使用、详解等。
  2. 生成器函数如何像async await一样处理异步。

生成器函数 Generator

Generator 基础入门

所谓 Generator 函数,最大特点就是可以交出函数的执行权(即拥有暂停函数执行的效果)。

function* gen() {yield 1;yield 2;yield 3;
}let g = gen();g.next(); // { value: 1, done: false }
g.next(); // { value: 2, done: false }
g.next(); // { value: 3, done: false }
g.next(); // { value: undefined, done: true }
g.next(); // { value: undefined, done: true }

首先调用生成器函数会返回一个 Generator { } 生成器实例对象。

所谓返回的 g 生成器对象可以简单的将它理解成为类似这样的一个对象结构:

{next: function () {return {done:Boolean, // done表示生成器函数是否执行完毕 它是一个布尔值value: VALUE, // value表示生成器函数本次调用返回的值}}
}
  • 首先,通过 let g = gen() 调用生成器函数创建了一个生成器对象 g ,此时 g 拥有 next 上述结构的 next 方法。

  • 之后,生成器对象中的 next 方法每次调用会返回一次 { value: VALUE, done: boolean }的对象。

每次调用生成器对象的 next 方法会返回一个上述类型的 object:

其中 done 表示生成器函数是否执行完毕,而 value 表示生成器函数中本次 yield 对应的值。
每当调用 g.next() 方法时,生成器函数紧跟着上一次进行执行,直到函数碰到 yield 关键值。

yield 关键字会停止函数执行并将 yield 后的值返回作为本次调用 next 函数的 value 进行返回。

同时,如果本次调用 g.next() 导致生成器函数执行完毕,那么此时 done 会变成 true 表示该函数执行完毕,反之则为 false 。

需要额外注意的是,当第四次调用迭代器 g.next() 时,因为第三次 g.next() 结束时生成器函数已经执行完毕了。所以再次调用 g.next() 时,由于函数结束 done 会变为 true 。同时因为函数不存在返回值,所以 value 为 undefined。

Generator 函数的返回值

function* gen() {const a = yield 1;console.log(a,'this is a')const b = yield 2;console.log(b,'this is b')const c = yield 3;console.log(c,'this is c')
}let g = gen();g.next(); // { value: 1, done: false }
g.next('param-a'); // { value: 2, done: false }
g.next('param-b'); // { value: 3, done: false }
g.next('param-c'); // { value: undefined, done: true }// 控制台会打印:
// param-a this is a
// param-b this is b
// param-c this is c

这里,我们来看看调用生成器对象的 next 方法传入参数时究竟会发生什么事情。

上文我们提到过,生成器函数中的 yield 关键字会暂停函数的运行,简单来说比如我们第一次调用 g.next() 方法时函数会执行到 yield 1 语句,此时函数会被暂停。

当第二次调用 g.next() 方法时,生成器函数会继续从上一次暂停的语句开始执行。这里有一个需要注意的点:当生成器函数恢复执行时,因为上一次执行到 const a = yield 1 语句的右半段并没有给 const a进行赋值。

比如在 g.next(‘param-a’) 传入的参数 param-a 会让生成器函数重新执行时,上一次 yield 语句的返回值进行执行。

简单来说,也就是调用 g.next('param-a')恢复函数执行时,相当于将生成器函数中的 const a = yield 1; 变成 const a = 'param-a'; 进行执行。

这样,第二次调用 g.next('param-a')时自然就打印出了 param-a this is a

同样当第三次调用 g.next('param-b') 时,本次调用 next 函数传入的参数会被当作 yield 2 运算结果赋值给 b 变量,执行到打印时会输出 param-b this is b

同理 g.next('param-c') 会输出 param-c this is c

总而言之,当为 next 传递值进行调用时,传入的值会被当作上一次生成器函数暂停时 yield 关键字的返回值处理。

自然,第一次调用 g.next() 传入参数是毫无意义的。因为首次调用 next 函数时,生成器函数并没有任何执行自然也没有 yield 关键字处理。

接下来看看所谓的生成器函数返回值:

function* gen() {const a = yield 1;console.log(a, 'this is a');const b = yield 2;console.log(b, 'this is b');const c = yield 3;console.log(c, 'this is c');return 'resultValue'
}let g = gen();g.next(); // { value: 1, done: false }
g.next('param-a'); // { value: 2, done: false }
g.next('param-b') // { value: 3, done: false }
g.next() // { value: 'resultValue', done: true }
g.next() // { value: undefined, done: true }

当生成器函数存在 return 返回值时,我们会在第四次调用 g.next() 函数恢复执行,此时生成器函数继续执行函数执行完毕。

此时自然 done 会变为 true 表示生成器函数已经执行完毕,之后,由于函数存在返回值所以本次的 value 会变为 ‘resultValue’ 。

也就是当生成器函数执行完毕时,原本本次调用 next 方法返回的 {done:true,value:undefined} 变为了{ done:true,value:'resultValue'}

Generator 异步解决方案

function promise1() {return new Promise((resolve) => {setTimeout(() => {resolve('1');}, 1000);});
}function promise2(value) {return new Promise((resolve) => {setTimeout(() => {resolve('value:' + value);}, 1000);});
}function* fn() {const value = yield promise1();const result = yield promise2(value);return result;
}

我们来看看上述的代码,fn 函数是不是稍微有一些 async 函数的影子。

假如 fn() 方法和 async 函数行为一致,返回的 result 同样是一个 Promise 并且保持上诉代码的写法,我们应该如何做?


上边我们提到过生成器函数具有可暂停的特点,当调用生成器函数后会返回一个生成器对象。每次调用生成器对象的 next 方法,生成器函数才会继续往下执行直到碰到下一个 yield 语句,同时每次调用生成器对象的 next(param) 方法时,我们可以传入一个参数作为上一次 yield 语句的返回值。

利用上述特性,我们可以写出如下的代码:

function promise1() {return new Promise((resolve) => {setTimeout(() => {resolve('1');}, 1000);});
}function promise2(value) {return new Promise((resolve) => {setTimeout(() => {resolve('value:' + value);}, 1000);});
}function* fn() {const value = yield promise1();const result = yield promise2(value);return result;
}function asyncGen(fn) {const g = fn(); // 调用传入的生成器函数 返回生成器对象// 期望返回一个Promisereturn new Promise((resolve) => {// 首次调用 g.next() 方法,会执行生成器函数直到碰到第一个yield关键字// 这里会执行到 yield promise1() 同时将 promise1() 返回为迭代器对象的 value 值const { value, done } = g.next();// 因为value为Promise 所以可以等待promise完成后,在then函数中继续调用 g.next(res) 恢复生成器函数继续执行value.then((res) => {// 同时第二次调用g.next() 时是在上一次返回的promise.then中// 可以拿到上一次Promise的value值也就是 '1'// 传入g.next('1') 作为上一次yield的值 这里相当于 const value = '1'const { value, done } = g.next(res);// 同理,继续上述过程value.then(resolve);});});
}asyncGen(fn).then((res) => console.log(res)); // value: 1

我们通过定义了一个 asyncGen 函数来包裹 fn 生成器函数,利用了生成器函数结合 yield 关键字可以被暂停的特点,同时结合 Promise.then 方法的特性实现了类似于 async 函数的异步写法。

看上去它和 async 很像对吧,不过目前的代码存在一个致命的问题:

asyncGen 函数并不具备通用性,上边的例子中 fn 函数内部包裹了两层 yield 处理的 promise,在 asyncGen 函数内部同样两次调用 g.next() 方法。

如果我们包裹三层 yield 处理的 Promise ,那我是不是重新书写 asyncGen 函数逻辑。又或者 fn 中存在比如 yield '1' 并不是 Promise 的语句,那么我们当作 Promise 使用 then 方法处理肯定是会报错。

这样的方法不具备任何通用性,所以在实际项目中没有人会这样去组织异步代码。但是通过这个例子我相信你已经可以初步了解 Generator 生成器函数是如何结合 Promise 来作为异步解决方案的。

继续优化代码

function co(gen) {return new Promise((resolve, reject) => {const g = gen();function next(param) {const { done, value } = g.next(param);if (!done) {// 未完成 继续递归Promise.resolve(value).then((res) => next(res));} else {// 完成直接重置 Promise 状态resolve(value);}}next();});
}co(fn).then((res) => console.log(res));

在函数 co 中,返回了一个 Promise 来作为包裹函数的返回值,同时首次调用 co 函数时会调用 gen() 得到对应的生成器对象。

然后定义了一次 next 方法,在 next 函数内部只要迭代器未完成那么此时我们就会在 value 的 then 方法中在此递归调用该 next 函数。

函数中稍微有三点需要大家额外注意:

  • 首先我们可以看到 next 函数接受传入的一个 param 的参数。

这是因为我们使用 Generator 来处理异步问题时,通过 const a = yield promise 将 promise 的 resolve 值交给 a ,所以我们需要在每次 then 函数中将 res 传递给下一次的 next(res) 作为上次 yield 的返回值。

  • 其次, Promise.resolve 将 value 进行了一层包裹,这是因为当生成器函数中的 yield 方法后紧挨的并不是 Promise 时,此时我们需要统一当作 Promise 来处理,因为需要统一调用 .then 方法。

  • 最后,首次调用 next() 方法时,并没有传入 param 参数。

总结

生成器函数具有可暂停的特点,当调用生成器函数后会返回一个生成器对象。每次调用生成器对象的 next 方法,生成器函数才会继续往下执行直到碰到下一个 yield 语句。调用next方法会返回一个对象,其中 done 表示生成器函数是否执行完毕,而 value 表示生成器函数中本次 yield 对应的值。

同时每次调用生成器对象的 next(param) 方法时,可以传入一个参数作为上一次 yield 语句的返回值。

所以Async await 本质上还是利用 Generator 函数内部可以被暂停执行的特性结合 Promise.then 中进行递归调用从而实现 Async await 的语法糖。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 多线程相关面试题
  • 免费扫描试卷的软件有哪些?5个软件帮助你轻松进行试卷扫描
  • GO HTTP库使用
  • Jmeter_循环获取请求接口的字段,并写入文件
  • golang学习笔记11——Go 语言的并发与同步实现详解
  • 信号与槽,QMainWindow中常用类的使用
  • SpringCache源码解析(三)——@EnableCaching
  • Vue 中实现视频播放的艺术
  • git使用手册
  • 离线安装NuGet组件方法
  • 大学生租房平台:SpringBoot技术实现详解
  • Anthropic 的 Claude AI 如何可能超过 OpenAI 的 ChatGPT?
  • 网络拓扑结构介绍
  • C++继承问题
  • three.js线框模式
  • 【mysql】环境安装、服务启动、密码设置
  • 【跃迁之路】【585天】程序员高效学习方法论探索系列(实验阶段342-2018.09.13)...
  • - C#编程大幅提高OUTLOOK的邮件搜索能力!
  • C++回声服务器_9-epoll边缘触发模式版本服务器
  • input的行数自动增减
  • magento 货币换算
  • React-Native - 收藏集 - 掘金
  • Selenium实战教程系列(二)---元素定位
  • SQLServer之创建显式事务
  • windows下mongoDB的环境配置
  • 后端_MYSQL
  • 解析 Webpack中import、require、按需加载的执行过程
  • 融云开发漫谈:你是否了解Go语言并发编程的第一要义?
  • 小程序 setData 学问多
  • 一个SAP顾问在美国的这些年
  • 移动端解决方案学习记录
  • 阿里云ACE认证之理解CDN技术
  • 微龛半导体获数千万Pre-A轮融资,投资方为国中创投 ...
  • ​3ds Max插件CG MAGIC图形板块为您提升线条效率!
  • ​一文看懂数据清洗:缺失值、异常值和重复值的处理
  • # AI产品经理的自我修养:既懂用户,更懂技术!
  • #Java第九次作业--输入输出流和文件操作
  • (03)光刻——半导体电路的绘制
  • (1)SpringCloud 整合Python
  • (Redis使用系列) Springboot 实现Redis 同数据源动态切换db 八
  • (Redis使用系列) Springboot 整合Redisson 实现分布式锁 七
  • (vue)页面文件上传获取:action地址
  • (翻译)terry crowley: 写给程序员
  • (介绍与使用)物联网NodeMCUESP8266(ESP-12F)连接新版onenet mqtt协议实现上传数据(温湿度)和下发指令(控制LED灯)
  • (亲测有效)推荐2024最新的免费漫画软件app,无广告,聚合全网资源!
  • (实战篇)如何缓存数据
  • (一)Kafka 安全之使用 SASL 进行身份验证 —— JAAS 配置、SASL 配置
  • (原創) 如何使用ISO C++讀寫BMP圖檔? (C/C++) (Image Processing)
  • .gitignore文件设置了忽略但不生效
  • .net 4.0 A potentially dangerous Request.Form value was detected from the client 的解决方案
  • .NET I/O 学习笔记:对文件和目录进行解压缩操作
  • .NET MAUI Sqlite程序应用-数据库配置(一)
  • .NET是什么
  • @configuration注解_2w字长文给你讲透了配置类为什么要添加 @Configuration注解
  • @SpringBootApplication 包含的三个注解及其含义