深入探讨Generator高逼格操作

es6 中引入了Generator Function(生成器函数)这个新特性,这篇文章会从以下4个点介绍,文章篇幅会有点长,请备好电源🤖

  1. 理解generator✅
  2. 如何用generator特性实现异步请求✅
  3. generator结合co库实现实现异步请求,自动流程✅
  4. generator其他应用✅

1.理解Generator

我们先上段代码,先瞧瞧generator是什么样子,当然你也可以把它叫做生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
let tell=function* (){
yield 'a';
yield 'b';
return 'c'
};
let k=tell();
k.next() //{value: "a", done: false}
k.next() //{value: "b", done: false}
k.next() //{value: "c", done: true}
k.next() //{value: undefined, done: true}
}

不知大家有没有见过这种写法,反正我第一次看到也有点懵.首先上面定义了一个function* (),切记generator并不是函数,它返回的是一个Iterator对象,这个我们下文会解释.这里有必要强调这一点,此外我们还看到yield这个关键字,干什么用的呢,next又是做什么,带着这些困惑我们先看看这段代码是如何运行的

  • tell()表示创建一个generator对象后,进入”暂停”状态

  • 当调用到next(),会执行yield,返回后面的”a”,返回的value就是它的值,done则表示是否最后一个值,类型是布尔值

  • 当第三次调用next,很明显是最后一个值了,所以返回true,第四次因为压根就找不到值所以返回了’undefined’

1.1generator传参

我们也可以在next的时候传递参数,如果看懂了下面的示例相信大家对generator已经有个深刻的理解了

1
2
3
4
5
6
7
8
9
10
11
12
{
function* paramGenerator() {
console.log(yield+"1");
console.log(yield+"2");
console.log(yield+"3");
}
let log = paramGenerator()
log.next()
log.next("a")
log.next("b")
log.next("c")
}

恩,给大家10秒钟的时间想想会输出什么

1
2
3
4
5
6
7
8
9
10
11
log.next()
Object {value: 1, done: false}
log.next("a")
a
Object {value: 2, done: false}
log.next("b")
b
Object {value: 3, done: false}
log.next("c")
c
Object {value: undefined, done: true}

恭喜你答对了,ok,我们来说说为什么会这样输出

首先,当执行第一个next的时候,generator第一个yield被激活,返回了{value: 1, done: false},紧接立刻进入”暂停”状态,第二个next我们传入了字符串a,执行了第一个console.log语句输出了a,接着yield被激活返回{value: 2, done: false},这一点相信大家都没有疑问,接着它又进入”暂停”状态,同理,第三个next我们继续传入参数”b”,执行第二个console语句输出了b,接着yield被激活返回{value: 3, done: false}.当我们执行第四个语句时,首先执行了第三个console输出了c,这时候value已经没有值所以返回”undefined”,done则表示已经是最后一个值所以返回true.

1.2 generator与Iterator关系

前面提到了generator返回是一个Iterator对象,而iterator拥有next的方法,所以我们才可以调用.说到了Iterator,这里要提到Symbol.iterator这个es6的数据类型,迭代协议规定了一些内置类型具有默认迭代行为,而其他类型(如Object)不具有(这个其实很好理解,object就是让我们去往它那填充数据,它肯定不知道我们要填充什么类型数据啦~),带有@@iterator方法的内置类型有:

stack

说那么多不如举个🌰,see:

1
2
3
4
5
6
7
{
let arr=['hello','iterator'];
let map=arr[Symbol.iterator]();
console.log(map.next()); //{value: "hello", done: false}
console.log(map.next()); //{value: "iterator", done: false}
console.log(map.next()); //{value: undefined, done: true}
}

怎么,看到它的输出是不是觉得很熟悉,没错前面我们generator调用next就是这种格式,同时也印证了generator返回是一个Iterator对象.
arr[Symbol.iterator]这个写法表示了这个arr对象具备了迭代的能力,那后面的括号是什么意思,表示arr数组调用iterator这个接口,这个接口已经内部实现好了.那么,我们可不可自定义iterator接口呢.恩可以的,同样举个🌰 see:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
let obj={
start:[1,3,2],
end:[7,9,8],
[Symbol.iterator](){
let self=this;
let index=0;
let arr=self.start.concat(self.end);
let len=arr.length;
return {
next(){
if(index<len){
return {
value:arr[index++],
done:false
}
}else{
return {
value:arr[index++],
done:true
}
}
}
}
}
}
for(let key of obj){
console.log(key); //1,3,2,7,9,8
}
}

不知道大家有没有想过,for…of能不能遍历对象呢?答案是不能的,如果你强行要这么做,会报一个obj[Symbol.iterator] is not a function的错误.代码看一下应该就懂我就不再过多解释了哈哈

2.如何用generator特性实现异步请求

我们先回顾下传统实现异步操作的方式,一般我们会想到回调事件触发这两种形式.这里我们主要谈论回调函数,事件触发其实就是采用事件驱动模式,通过一个事件触发另一个事件.

2.1传统Ajax实现异步操作

  • 业务场景: 执行完a后执行b
1
2
3
4
5
6
7
8
9
let ajax=function(callback){
console.log('执行a');
setTimeout(function () {
callback&&callback.call()
}, 1000);
};
ajax(function(){
console.log('执行b');
})

代码比较简单,但试想如果执行b后还有c,d,e呢?那代码将会很复杂并且难以维护.为了解决这个问题,所以Promise诞生了.

2.2Promise实现异步操作

  • 直接上代码
1
2
3
4
5
6
7
8
9
10
11
12
13
{
let ajax=function(){
console.log('执行a');
return new Promise(function(resolve,reject){
setTimeout(function () {
resolve()
}, 1000);
})
};
ajax().then(function(){
console.log('promise','执行b');
})
}
  1. promise里面的匿名函数有两个参数resolvereject,resolve表示表示执行下一步操作,reject当然就是中断操作~;
  2. ajax().then…. 意思是当执行成功后会调用Promise实例的then方法
  • 如果后面也需要执行c,d,e…呢,代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
let ajax=function(){
console.log('执行a');
return new Promise(function(resolve,reject){
setTimeout(function () {
resolve()
}, 1000);
})
};
ajax()
.then(function(){
return new Promise(function(resolve,reject){
setTimeout(function () {
console.log('执行b')
resolve()
}, 2000);
});
})
.then(function(){
console.log('执行c')
})
....
}

试想一下,如果执行到某一步抛异常了该怎么处理? 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
let ajax=function(num){
console.log('执行');
return new Promise(function(resolve,reject){
if(num>5){
resolve()
}else{
throw new Error('出错了')
}
})
}
ajax(6).then(function(){
console.log('log',6);
}).catch(function(err){
console.log('catch',err);
});
ajax(3).then(function(){
console.log('log',3);
}).catch(function(err){
console.log('catch',err);
});
}

Promise还有个更高级的用法Promise.all(),它返回也是一个promise实例,它会把多个promise实例当成一个实例,等待所有promise实例都加载执行完后才执行一个新的Promise对象,所以它就可以调用then方法.业务场景:加载网络图片.代码就不贴了毕竟不是这篇文章的主要讨论内容.

2.3 generator特性实现异步请求

generator让人第一感觉就是用同步的写法做异步处理为行为,我们将前面说的方式用generator改写对比看看

1
2
3
4
5
6
7
8
{
co(function* () {
const c1 = yield ajax(5)
const c2 = yield ajax(6)
const c3 = yield ajax(3)
}
}

这里得我们用到了co库,它能做到自驱动流程,省去每次都得手动调用next,这一点下文会详细讲到co库

3.generator结合co库实现实现异步请求,自动流程

co库是著名程序员 TJ Holowaychuk 于2013年6月发布的一个小工具,用于 Generator 函数的自动执行,我们将上面的代码稍微改一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
const co = require('co')
let c = function* () {
const c1 = yield ajax(5)
const c2 = yield ajax(6)
const c3 = yield ajax(3)
}
const cc = co(c)
cc.then(data=>{
....
})
}

co库返回的是一个Promise对象,所以then的操作大家一看就知道怎么回事了,那么co库究竟做了什么操作呢? see:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function run(generator) {
const g = generator()
function next(err, data) {
const result = g.next(data)
if (result.done) {
return
}
result.value(next)
}
next()
}
//thunkify是一个开源库,其实就是一个经过封装处理的thunk函数
const readFileThunk = thunkify(fs.readFile)
const gen = function* () {
const r1 = yield readFileThunk('data1.json')
console.log(r1.toString())
}
// 启动执行
run(gen)

看到这段代码理解起来有点难度,next(err, data)是什么意思?g.next(data)又是什么,头都大了.我们来拆分之后你就懂了.首先我们要引进thunk这个函数,它的作用在于将执行参数与回调参数分为两个函数去调用,举个读取文件的🌰:

1
2
fs.readFile('data.json', 'utf-8', (err, data) => {
})

用thunk函数改写后,see:

1
2
3
4
5
6
7
8
9
10
let thunk = (file,meta)=>{
return function (callback) {
fs.readFile(fileName, meta, callback)
}
}
const fileRead = thunk('data.json','utf-8')
fileRead((err,data)=>{
...
)}

ok,结合上面代码完整代码是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let thunk = (file,meta)=>{
return function (callback) {
fs.readFile(fileName, meta, callback)
}
}
function run(generator) {
const g = generator()
function next(err, data) {
const result = g.next(data)
if (result.done) {
return
}
result.value(next)
}
next()
}
const gen = function* () {
const r1 = yield thunk('data.json','utf-8')
console.log(r1.toString())
}
// 启动执行
run(gen)

g.next返回的是一个thunk函数,从代码可以清楚看到,result.value 返回也是thunk函数,传入的next就是它的回调函数

4.generator其他应用

4.1 抽奖

举个🌰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
let draw = function(count){
//具体抽奖逻辑
console.info(`剩余${count}次`)
}
let residue = function* (){
while(count>0){
count --;
yield draw(count);
}
}
let star = residue(3);
let btn = document.createElement('button');
btn.id = "start";
btn.textContent = "抽奖";
doucment.body.appendChild(btn);
document.getElementById('start').addEventListener('click',function(){
start.next();
},false)
}

从代码可以看到我们并没有创建count作为全局变量,为什么这么处理大家不妨想想,我们在draw这个函数就只是单单抽奖的逻辑,抽奖次数我们直接在初始化generator的时候给它传参,代码看起来非常干净(可耻的炫耀一下)

4.2 长轮洵

当服务端某个数据定期会发生变化,前端需要做的工作就是会在它发生变化的时候实时取得数据并展现出来,同样举个🌰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
let ajax = function* (){
yield new Promise(function(resolve,reject){
setTimeout(function() {
resolve({code:0})
}, 200);
})
}
let fetch = function(){
let g = ajax();
let step = g.next();
step.value.then((d)=>{
if(d.code != 0){
setTimeout(function() {
console.log('wait')
fetch()
}, 1000);
}else {
console.log(d); //{code:0}
}
})
}
}

我们将generator和promise进行结合实现异步处理,g.next()会对generator进行一次迭代向服务端查询,step.value取得promise实例,通过then函数对拿到的数据做处理就行了

后话:generator和co结合应用在了koa1,koa2则是用了es7 的提案async/await来做异步开发,由于koa2正安排进我的学习计划中,那就等后面学到哪更到哪吧,第一篇koa2的文章已经更新了戳这里,敬请留意后续更新~

感谢您的阅读,本文由 lynhao 原创提供。如若转载,请注明出处:lynhao(http://www.lynhao.cn
进击Koa2系列[一]
mongodb环境搭建(补充)