前两年大量的在写Generator+co,用它来写一些类似同步的代码
但实际上,Generator并不是被造出来干这个使的,不然也就不会有后来的asyncawait
Generator是一个可以被暂停的函数,并且何时恢复,由调用方决定
希望本文可以帮助你理解Generator究竟是什么,以及怎么用

放一张图来表示我对Generator的理解:

一个咖啡机,虽说我并不喝咖啡,可惜找不到造王老吉的机器-.-

我所理解的Generator咖啡机大概就是这么的一个样子的:

  1. 首先,我们往机器里边放一些咖啡豆
  2. 等我们想喝咖啡的时候,就可以按开关(gen.next()),机器开始磨咖啡豆、煮咖啡、接下来就得到咖啡了
  3. 等接满了一杯咖啡后,阀门就会自动关闭(yield)
  4. 如果你一开始往机器里边放的咖啡豆很多的话,此时,机器里边还是会有一些剩余的,下次再想喝还可以继续按开关,执行(磨豆、煮咖啡、接咖啡)这一套操作

Generator将上述咖啡机实现一下:

function * coffeeMachineGenerator (beans) {
do {
yield cookCoffee()
} while (--beans) // 煮咖啡
function cookCoffee () {
console.log('cooking') return 'Here you are'
}
} // 往咖啡机放咖啡豆
let coffeeMachine = coffeeMachineGenerator(10) // 我想喝咖啡了
coffeeMachine.next() // 我在3秒后还会喝咖啡
setTimeout(() => {
coffeeMachine.next()
}, 3 * 1e3)

代码运行后,我们首先会得到一条cookinglog
然后在3s后会再次得到一条log

这就解释了Generator是什么:
一个可以暂停的迭代器
调用next来获取数据(我们自己来决定是否何时煮咖啡
在遇到yield以后函数的执行就会停止(接满了一杯,阀门关闭
我们来决定何时运行剩余的代码next什么时候想喝了再去煮

这是Generator中最重要的特性,我们只有在真正需要的时候才获取下一个值,而不是一次性获取所有的值

Generator的语法

声明Generator函数有很多种途径,最重要的一点就是,在function关键字后添加一个*

function * generator () {}
function* generator () {}
function *generator () {} let generator = function * () {}
let generator = function* () {}
let generator = function *() {} // 错误的示例
let generator = *() => {}
let generator = ()* => {}
let generator = (*) => {}

或者,因为是一个函数,也可以作为一个对象的属性来存在:

class MyClass {
* generator() {}
*generator2() {}
} const obj = {
*generator() {}
* generator() {}
}

generator的初始化与复用

一个Generator函数通过调用两次方法,将会生成两个完全独立的状态机
所以,保存当前的Generator对象很重要:

function * generator (name = 'unknown') {
yield `Your name: ${name}`
} const gen1 = generator()
const gen2 = generator('Niko Bellic') gen1.next() // { value: Your name: unknown , done: false}
gen2.next() // { value: Your name: Niko Bellic, done: false}

Method: next()

最常用的next()方法,无论何时调用它,都会得到下一次输出的返回对象(在代码执行完后的调用将会始终返回{value: undefined, done: true})。

next总会返回一个对象,包含两个属性值:
valueyield关键字后边表达式的值
done :如果已经没有yield关键字了,则会返回true .

function * generator () {
yield 5
return 6
} const gen = generator() console.log(gen.next()) // {value: 5, done: false}
console.log(gen.next()) // {value: 6, done: true}
console.log(gen.next()) // {value: undefined, done: true}
console.log(gen.next()) // {value: undefined, done: true} -- 后续再调用也都会是这个结果

作为迭代器使用

Generator函数是一个可迭代的,所以,我们可以直接通过for of来使用它。

function * generator () {
yield 1
yield 2
return 3
} for (let item of generator()) {
item
} //
//

return不参与迭代
迭代会执行所有的yield,也就是说,在迭代后的Generator对象将不会再返回任何有效的值

Method: return()

我们可以在迭代器对象上直接调用return(),来终止后续的代码执行。
return后的所有next()调用都将返回{value: undefined, done: true}

function * generator () {
yield 1
yield 2
yield 3
} const gen = generator() gen.return() // {value: undefined, done: true}
gen.return('hi') // {value: "hi", done: true}
gen.next() // {value: undefined, done: true}

Method: throw()

在调用throw()后同样会终止所有的yield执行,同时会抛出一个异常,需要通过try-catch来接收:

function * generator () {
yield 1
yield 2
yield 3
} const gen = generator() gen.throw('error text') // Error: error text
gen.next() // {value: undefined, done: true}

Yield的语法

yield的语法有点像return,但是,return是在函数调用结束后返回结果的
并且在调用return之后不会执行其他任何的操作

function method (a) {
let b = 5
return a + b
// 下边的两句代码永远不会执行
b = 6
return a * b
} method(6) //
method(6) //

而yield的表现则不一样

function * yieldMethod(a) {
let b = 5
yield a + b
// 在执行第二次`next`时,下边两行则会执行
b = 6
return a * b
} const gen = yieldMethod(6)
gen.next().value //
gen.next().value //

yield*

yield*用来将一个Generator放到另一个Generator函数中执行。
有点像[...]的功能:

function * gen1 () {
yield 2
yield 3
} function * gen2 () {
yield 1
yield * gen1()
yield 4
} let gen = gen2() gen.next().value //
gen.next().value //
gen.next().value //
gen.next().value //

yield的返回值

yield是可以接收返回值的,返回值可以在后续的代码被使用
一个诡异的写法

function * generator (num) {
return yield yield num
} let gen = generator(1) console.log(gen.next()) // {value: 1, done: false}
console.log(gen.next(2)) // {value: 2, done: false}
console.log(gen.next(3)) // {value: 3, done: true }

我们在调用第一次next时候,代码执行到了yield num,此时返回num
然后我们再调用next(2),代码执行的是yield (yield num),而其中返回的值就是我们在next中传入的参数了,作为yield num的返回值存在。
以及最后的next(3),执行的是这部分代码return (yield (yield num)),第二次yield表达式的返回值。

一些实际的使用场景

上边的所有示例都是建立在已知次数的Generator函数上的,但如果你需要一个未知次数的Generator,仅需要创建一个无限循环就够了。

一个简单的随机数生成

比如我们将实现一个随机数的获取:

function * randomGenerator (...randoms) {
let len = randoms.length
while (true) {
yield randoms[Math.floor(Math.random() * len)]
}
} const randomeGen = randomGenerator(1, 2, 3, 4) randomeGen.next().value // 返回一个随机数

代替一些递归的操作

那个最著名的斐波那契数,基本上都会选择使用递归来实现
但是再结合着Generator以后,就可以使用一个无限循环来实现了:

function * fibonacci(seed1, seed2) {
while (true) {
yield (() => {
seed2 = seed2 + seed1;
seed1 = seed2 - seed1;
return seed2;
})();
}
} const fib = fibonacci(0, 1);
fib.next(); // {value: 1, done: false}
fib.next(); // {value: 2, done: false}
fib.next(); // {value: 3, done: false}
fib.next(); // {value: 5, done: false}
fib.next(); // {value: 8, done: false}

与async/await的结合

再次重申,我个人不认为async/await是Generator的语法糖。。

如果是写前端的童鞋,基本上都会遇到处理分页加载数据的时候
如果结合着Generator+asyncawait,我们可以这样实现:

async function * loadDataGenerator (url) {
let page = 1 while (true) {
page = (yield await ajax(url, {
data: page
})) || ++page
}
} // 使用setTimeout模拟异步请求
function ajax (url, { data: page }) {
return new Promise((resolve) => {
setTimeout(_ => {
console.log(`get page: ${page}`);
resolve()
}, 1000)
})
} let loadData = loadDataGenerator('get-data-url') await loadData.next()
await loadData.next() // force load page 1
await loadData.next(1)
await loadData.next() // get page: 1
// get page: 2
// get page: 1
// get page: 2

这样我们可以在简单的几行代码中实现一个分页控制函数了。
如果想要从加载特定的页码,直接将page传入next即可。

小记

Generator还有更多的使用方式,(实现异步流程控制、按需进行数据读取)
个人认为,Generator的优势在于代码的惰性执行,Generator所实现的事情,我们不使用它也可以做到,只是使用Generator后,能够让代码的可读性变得更好、流程变得更清晰、更专注于逻辑的实现。

如果有什么不懂的地方 or 文章中一些的错误,欢迎指出

参考资料

  1. Javascript (ES6) Generators — Part I: Understanding Generators
  2. What are JavaScript Generators and how to use them

文章示例代码

Generator的正确打开方式的更多相关文章

  1. C++11随机数的正确打开方式

    C++11随机数的正确打开方式 在C++11之前,现有的随机数函数都存在一个问题:在利用循环多次获取随机数时,如果程序运行过快或者使用了多线程等方法,srand((unsigned)time(null ...

  2. iOS开发小技巧--相机相册的正确打开方式

    iOS相机相册的正确打开方式- UIImagePickerController 通过指定sourceType来实现打开相册还是相机 UIImagePickerControllerSourceTypeP ...

  3. Xcode 的正确打开方式——Debugging(转载)

    Xcode 的正确打开方式——Debugging   程序员日常开发中有大量时间都会花费在 debug 上,从事 iOS 开发不可避免地需要使用 Xcode.这篇博客就主要介绍了 Xcode 中几种能 ...

  4. C#语法——泛型的多种应用 C#语法——await与async的正确打开方式 C#线程安全使用(五) C#语法——元组类型 好好耕耘 redis和memcached的区别

    C#语法——泛型的多种应用   本篇文章主要介绍泛型的应用. 泛型是.NET Framework 2.0 版类库就已经提供的语法,主要用于提高代码的可重用性.类型安全性和效率. 泛型的定义 下面定义了 ...

  5. InnoDB缓冲池预加载在MySQL 5.7中的正确打开方式

    InnoDB缓冲池预加载在MySQL 5.7中的正确打开方式 https://mp.weixin.qq.com/s/HGa_90XvC22anabiBF8AbQ 在这篇文章里,我将讨论在MySQL 5 ...

  6. Console控制台的正确打开方式

    Console控制台的正确打开方式 console对象提供了访问浏览器调试模式的信息到控制台 -- Console对象 |-- assert() 如果第一个参数断言为false,则在控制台输出错误信息 ...

  7. 任务队列和异步接口的正确打开方式(.NET Core版本)

    任务队列和异步接口的正确打开方式 什么是异步接口? Asynchronous Operations Certain types of operations might require processi ...

  8. (一)Redis for Windows正确打开方式

    目录 (一)Redis for Windows正确打开方式 (二)Redis for 阿里云公网连接 (三)Redis for StackExchange.Redis 下载地址 官网.中文网1 及 中 ...

  9. List的remove()方法的三种正确打开方式

    转: java编程:List的remove()方法的三种正确打开方式! 2018年08月12日 16:26:13 Aries9986 阅读数 2728更多 分类专栏: leetcode刷题   版权声 ...

随机推荐

  1. 开源数字媒体资产管理系统:Razuna安装方法

    Razuna以一个使用Java语言编写的开源的数字媒体资产管理(Digital Asset Management)系统.在这里翻译一下它的安装步骤. Razuna包含以下版本: Razuna Stan ...

  2. mysql进阶(六)模糊查询的四种用法介绍

    mysql中模糊查询的四种用法介绍 这篇文章主要介绍了mysql中模糊查询的四种用法,需要的朋友可以参考下. 下面介绍mysql中模糊查询的四种用法: 1 %: 表示任意0个或多个字符.可匹配任意类型 ...

  3. 【Qt编程】基于Qt的词典开发系列<一>--词典框架设计及成品展示

    去年暑假的时候,作为学习Qt的实战,我写了一个名为<我爱查词典>的词典软件.后来由于导师项目及上课等原因,时间不足,所以该软件的部分功能欠缺,性能有待改善.这学期重新拿出来看时,又有很多东 ...

  4. C++中const的实现机制深入分析

    via:http://www.jb51.net/article/32336.htm C语言以及C++语言中的const究竟表示什么?其具体的实现机制又是如何实现的呢?本文将对这两个问题进行一些分析,需 ...

  5. 一张图了解cocos2d坐标系

    一张图了解cocos2d坐标系 平面直角坐标系

  6. linux服务搭建----NFS服务搭建

    nfs  服务     //配置脚本         /etc/exports     //服务器  发布目录     1> 确定需要发布的目录(想要共享的目录)     例如:      想发 ...

  7. Socket层实现系列 — bind()的实现(一)

    bind()函数的使用方法很简单,但是它是怎么实现的呢? 笔者从应用层出发,沿着网络协议栈,分析了bind()的系统调用.Socket层实现,以及它的TCP层实现. 本文主要内容:bind()的系统调 ...

  8. Android高效率编码-第三方SDK详解系列(二)——Bmob后端云开发,实现登录注册,更改资料,修改密码,邮箱验证,上传,下载,推送消息,缩略图加载等功能

    Android高效率编码-第三方SDK详解系列(二)--Bmob后端云开发,实现登录注册,更改资料,修改密码,邮箱验证,上传,下载,推送消息,缩略图加载等功能 我的本意是第二篇写Mob的shareSD ...

  9. ROS探索总结(十三)——导航与定位框架

    导航与定位是机器人研究中的重要部分.         一般机器人在陌生的环境下需要使用激光传感器(或者深度传感器转换成激光数据),先进行地图建模,然后在根据建立的地图进行导航.定位.在ROS中也有很多 ...

  10. 安卓学习笔记一 Activity延迟转跳实现欢迎界面

    新人学习安卓,为了刚好的学习,现做如下笔记..同时希望自己的经验可以帮助新人们学习入门. 几乎每个app都有个欢迎界面,我们可以使用Activity转跳来实现. 首先建立一个MainActivity ...