前两年大量的在写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. [面试算法题]比较二叉树异同-leetcode学习之旅(5)

    问题描述 Given two binary trees, write a function to check if they are equal or not. Two binary trees ar ...

  2. 【Qt编程】Qt学习之窗口间的相互切换

    在用Qt设计GUI时,经常要设计两个窗口之间的相互切换,即可以从一个窗口跳转到另一个窗口,然后又从另一个窗口跳转回原窗口.下面我们来介绍具体的实现方法: 工程建立及功能描述: 首先,我们建立Qt  G ...

  3. Graph Cut and Its Application in Computer Vision

    Graph Cut and Its Application in Computer Vision 原文出处: http://lincccc.blogspot.tw/2011/04/graph-cut- ...

  4. 制药企业BI系统方案整体设计分享

    制药企业全面预算系统蓝图 全面掌控企业的各种业务活动,及时准确的展现它们的状况与趋势,评估其达成的效果.存在的问题与风险.支持数据的导入,多级上报等多种特色功能,同时通过统一的报表平台实现精细话的权限 ...

  5. C++ 传参时传内置类型时用传值(pass by value)方式效率较高

    来源:唐磊的个人博客<C++ 传参时传内置类型时用传值(pass by value)方式效率较高> 在<Effective C++>里提到对内置(C-like)类型在函数传参时 ...

  6. eclipse或者AS链接手机真机之后,logcat里面日志信息乱跳

    乱跳的日志信息不会对应用产生影响,但是它会影响视觉,影响查看logcat.那主要原因在哪里呢 ? 这是由于手机里面,正在的运行的进程太多导致的.^_^ 因此课件添加过滤器的作用之大. 对了,在logc ...

  7. ubuntu如何添加新的PPA

    首先要知道PPA源地址,比如: ppa:gwibber-daily/ppa 然后用apt-get指令添加: sudo add-apt-repository ppa:gwibber-daily/ppa ...

  8. Boyer-Moore算法

    1.概述 在用于查找子字符串的算法当中,BM(Boyer-Moore)算法是目前相当有效又容易理解的一种,一般情况下,比KMP算法快3-5倍. BM算法在移动模式串的时候是从左到右,而进行比较的时候是 ...

  9. 对于CocoaPods的简单理解,实践安装使用过程和常见问题

    (本文是自己通过其他文章进行的自我编辑和简单修改,请大家凑活看看) 一.什么是CocoaPods CocoaPods是iOS项目的依赖管理工具,该项目源码在Github上管理.开发iOS项目不可避免地 ...

  10. python标准库Beautiful Soup与MongoDb爬喜马拉雅电台的总结

    Beautiful Soup标准库是一个可以从HTML/XML文件中提取数据的Python库,它能够通过你喜欢的转换器实现惯用的文档导航,查找,修改文档的方式,Beautiful Soup将会节省数小 ...