generator 函数是 ES6 引入的特殊函数,核心特征是可以暂停执行并且在后续恢复执行,依靠 yield 关键字标记暂停点位,返回一个 generator 迭代器对象,它本身并不是专门的异步解决方案,但是可以配合 yield 把异步流程写成类似同步代码的写法,解决传统回调嵌套也就是回调地狱的痛点,generator 函数实现异步的主流方式是手动调用迭代器 next 方法,或者借助 co 这类第三方库自动执行,后续 async await 本质就是 generator 加自动执行器的语法糖,理解 generator 异步是读懂 async await 底层原理的关键。
之所以 generator 能够用来处理异步,根源在于它的执行控制权可以主动交还,普通函数一旦启动就会一直运行直到 return 或者执行完毕,中途无法暂停,generator 函数调用之后不会立刻执行函数体内代码,而是生成迭代器,每一次调用 next,代码就从上次暂停的 yield 位置继续向下运行,直到遇到下一个 yield 就再次暂停,并且把 yield 后面表达式的值通过迭代器的 value 属性返回,异步场景里我们可以把异步请求写在 yield 后面,等异步回调拿到结果之后,再调用 next 把结果传入迭代器,让 generator 继续往下执行,这样代码阅读顺序就和同步代码保持一致,不再需要层层嵌套回调。
generator 函数基础语法要点
generator 函数声明的写法是在 function 关键字后面添加星号,函数内部使用 yield 表达式,yield 可以暂停函数执行,yield 本身不是返回语句,它是暂停标记。
调用 generator 函数之后得到迭代器对象,迭代器对象拥有 next 方法,next 方法执行之后返回固定结构对象,包含两个属性,value 代表当前 yield 表达式的值,done 是布尔值,代表 generator 是否已经全部执行完成,done 为 true 的时候代表函数已经执行结束。
next 方法还支持传入参数,这个传入的参数会作为上一个 yield 表达式的返回值,这个特性是 generator 实现异步传值最关键的能力,很多初学者容易忽略这一点,上一次 yield 本身默认返回 undefined,只有下一次 next 传参,才会替换这个 yield 的返回结果。
generator 还有 throw 方法和 return 方法,throw 可以在 generator 内部抛出异常,内部可以用 try catch 捕获,return 方法会直接终止 generator,把 done 设置为 true。
generator 实现异步的第一种方式 手动 next 迭代
手动模式就是我们自己控制每一步,异步操作完成之后,在异步回调里面调用迭代器的 next,把异步拿到的数据传递进去。
执行步骤
第一步 声明 generator 函数,内部用 yield 包裹异步操作,一般异步操作会返回 promise 或者是回调风格的函数
第二步 调用 generator 函数创建迭代器实例
第三步 第一次调用 next 启动 generator,执行到第一个 yield 暂停
第四步 等待异步任务完成,拿到结果,再次调用 next,把结果传入,generator 继续向下执行
第五步 重复上面流程,直到 done 变成 true,整个 generator 流程结束
这个方案的缺点很明显,每一步异步都要手动写回调然后调用 next,异步步骤一多,代码还是会变繁琐,自动化程度很低,适合理解原理,生产项目很少直接这么写。
generator 实现异步的第二种方式 co 库自动执行
co 是 TJ Holowaychuk 开发的自动执行器,专门用来配合 generator 处理 promise 异步,它不需要开发者手动一次次调用 next,co 接收 generator 函数或者迭代器,自动识别 yield 后面的 promise,等待 promise 成功 resolve 之后,自动调用 next 把结果传入,继续执行后续代码,全部完成之后 co 本身返回一个 promise。
co 的核心运行逻辑
co 内部实现递归调用 next,检测 yield 后面的值类型,如果是 promise,就执行 then,promise 成功之后继续 next,如果是数组对象这种并发类型,co 会并行等待所有 promise 完成,再继续 generator。
使用步骤
第一步 安装或者引入 co 库
第二步 编写 generator 函数,yield 后面统一放置 promise 对象
第三步 把 generator 函数传入 co,co 执行之后返回 promise,可以链式 then 和 catch 捕获最终结果和异常
限制点 co 要求 yield 后面必须是 promise,thunk 函数,或者 co 支持的可等待类型,如果 yield 后面不是可识别对象,自动执行就会失效,这也是 co 最终被原生 async await 替代的重要原因。
generator 异步和 promise 的关系
generator 本身不具备等待异步完成的能力,它只是提供暂停恢复的执行机制,真正等待异步结果仍然依靠 promise,所以 generator 异步本质是 generator 执行控制能力加上 promise 异步状态管理的组合方案,单纯回调函数风格的异步也可以配合 generator,但是处理异常和并发会非常麻烦,业界标准做法都是 yield 后面写 promise。
generator 异步并发写法
generator 里面如果多个异步任务没有前后依赖,可以并行执行,写法就是同时创建多个 promise,放到数组里面 yield,co 支持等待 promise 数组全部完成,这样不会串行等待,节省总耗时,如果写成多个连续 yield,就是串行执行,上一个完成才执行下一个,两种模式要根据业务区分。
generator 异步异常处理
generator 内部可以使用 try catch 包裹 yield 对应的异步 promise,当 promise reject 的时候,异常会被内部 try catch 捕获,不需要在每一个异步单独写错误回调,相比传统回调地狱,错误处理更加集中,这点也是 generator 异步的优势,co 执行的时候如果 generator 内部没有捕获异常,那么 co 返回的外层 promise 就会触发 reject。
generator 异步的优缺点总结
优点
代码阅读顺序接近同步写法,解决回调嵌套,执行过程可以暂停,支持中途传参,适合理解异步执行原理,能够集中管理异常
缺点
原生没有自动执行能力,手动 next 非常繁琐,依赖 co 这类第三方库,语法认知门槛高,并发控制需要额外理解,浏览器和旧环境需要转译,最终被原生 async await 取代
async await 和 generator 的底层关联
async 函数本质就是语法糖,它内部底层思想和 generator 自动执行器思路一致,await 相当于 yield,async 函数自带自动执行器,不需要引入 co,原生支持,不需要手动调用 next,await 后面同样等待 promise,异常使用 try catch 捕获,所以学会 generator 异步,就可以理解 async await 不是全新概念,只是浏览器原生内置的 generator 自动执行版本。
【常见问题】
问题 1 generator 函数实现异步为什么不能直接等待异步结果必须配合 next 传参
回答 1 generator 函数本身只负责暂停和恢复执行,没有内置等待异步任务完成的能力,异步任务是独立运行的,异步任务返回结果之后,必须依靠下一次 next 方法传参,把结果赋值给上一个 yield 表达式,generator 才能拿到异步数据继续运行,所以 generator 异步的核心就是依靠 next 传参把异步结果回填进 generator 内部。
问题 2 co 库作为 generator 异步执行器,yield 后面为什么推荐写 promise
回答 2 co 库设计的时候主要识别 promise 对象,co 内部会监听 promise 的 resolve 和 reject 状态,promise 有标准化的成功失败状态,支持统一异常处理,如果 yield 后面不是 promise,co 无法识别异步完成时机,generator 异步流程就无法自动继续执行,所以 co 模式下 generator 异步标准写法就是 yield 后面放置 promise。
问题 3 generator 函数异步串行和并行有什么区别
回答 3 generator 异步串行是连续书写多个 yield,上一个 yield 对应的异步任务完成之后,generator 才会继续执行下一个 yield 异步任务,总耗时是所有异步任务耗时相加,generator 异步并行是一次性创建多个 promise 组成数组再 yield,所有异步任务同时启动,等待全部完成之后 generator 继续执行,总耗时取决于最慢那一个异步任务。
问题 4 generator 异步和 async await 是不是完全等价
回答 4 generator 异步加上 co 自动执行器在能力上和 async await 非常接近,但是并不是完全等价,generator 是原生语法,需要额外 co 库实现自动执行,async await 是 ES2017 原生语法,自带自动执行器,不需要第三方库,语法更简洁,async await 是 generator 异步方案标准化之后的原生语法糖。
问题 5 generator 函数异步可以使用 throw 方法处理异步错误吗
回答 5 generator 函数异步场景可以使用迭代器的 throw 方法,它会在 generator 内部抛出错误,可以被内部 try catch 捕获,但是 co 自动执行模式下一般推荐直接依靠 promise reject 配合内部 try catch 捕获,手动 next 模式才会经常用到迭代器 throw 来处理 generator 异步异常。
【免责申明】
本文内容基于 ES6 标准 generator 函数原理与 co 库的公开技术知识进行讲解,代码思路仅用于学习理解 generator 异步底层原理,不代表生产环境推荐方案,实际项目开发优先选择原生 async await,不同浏览器和转译环境存在兼容性差异,实际编码请结合项目环境测试验证,本文不构成任何工程选型强制建议。
本文来自作者[俗世百晓研习]投稿,不代表万里行信息网立场,如若转载,请注明出处:https://www.wlx315.cn/shcs/202611/7760.html
评论列表(4条)
我是万里行信息网的签约作者“俗世百晓研习”!
希望本篇文章《generator 函数以及 generator 函数的异步操作方法 深度原理与实战》能对你有所帮助!
本站[万里行信息网]内容主要涵盖:生活百科,小常识,生活小窍门,知识分享
本文概览:generator 函数是 ES6 引入的特殊函数,核心特征是可以暂停执行并且在后续恢复执行,依靠 yield 关键字标记暂停点位,返回一个 generator 迭代器对象,它本身并不是专门的异步解决方案,但是可以配合 yield 把异步流程写成类似同步代码的写法,解决传统回调嵌套也就是回调地狱的痛点,generator 函数实现异步的主流方式是手动调用迭代器 next 方法,或者借助 co 这类第三方库自动执行,后续 async await 本质就是 generator 加自动执行器的语法糖,理解 generator 异步是读懂 async await 底层原理的关键。