ES6基础系列 —— 理解Generator及其应用场景

今天给团队做的一次关于Generator的分享,整理至此。这次分享的目的是:循序渐进地让大家对ES6的Generator有一个初步的了解、然后加深理解、再通过一个run函数知晓其应用场景,最后明晰Generator带来的意义。

一、开篇

我们比较常遇见异步的两个场景:顺序调用和并行调用。

我们平时的异步处理一般是这样写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var getList1 = (success) => {
$.ajax({
url: 'http://sapi.bbeeii.com/martgoods/category/fightgroup.html',
type: 'GET',
dataType: 'jsonp',
jsonpCallback: 'bbeeiiMartgoodsCategoryGet',
success
});
};
var getList2 = (success) => {
$.ajax({
url: 'http://sapi.bbeeii.com/martshow/1-15----1.html',
type: 'GET',
dataType: 'jsonp',
jsonpCallback: 'bbeeiiMartshowGet',
success
});
};
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
// 顺序调用
getList1((resp1) => {
// do something
getList2(resp2 => {
// do something
// ..... 回调地狱
});
});

// 可能的并行调用
var getAllList = (getLists, callback) => {
const listLen = getLists.length;
const resultArr = Array.from({
length: listLen
});

getLists.forEach((getList, index) => {
getList((resp) => {
resultArr[index] = resp;

// 简单判断 中间要做各种兼容处理、错误判断,比较复杂
if (resultArr.filter((result) => result).length === listLen) {
callback(resultArr);
}
});
});
};

上面两个例子相对还比较简单,但当我们把场景搬到Node上,我们毫无疑问会遇到两个问题:

  1. 错误处理
  2. 回调地狱

二、async.js / Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 顺序调用
async.series([
(callback) => callback(null, 'one'),
(callback) => callback(null, 'two')
],
// optional callback
(err, results) => {
// results is now equal to ['one', 'two']
});

// 并行调用
async.parallel([
(callback) => setTimeout(() => callback(null, 'one'), 200),
(callback) => setTimeout(() => callback(null, 'two'), 100)
],
// optional callback
(err, results) => {
// the results array will equal ['one','two'] even though
// the second function had a shorter timeout.
});

我们可以理解为,async.js提供了异步流程控制,相当于上述开篇的进一步封装,但其仍是各种callback。

同理,Promise尽管提供了链式的调用,看起来告别了回调地狱,提供了非常大的便利,但本质还是“异步的编写方式”,写太多then也比较繁琐(因为不在本文主题之内,不做发散)。

三、Generator

开始之前

Generator最初步的了解,需要大家至少从 阮一峰老师的ES6 里了解对应的function*、yield、next、value、done这几个概念。

先抛一个最简单的理解:就把他看成一种新的语法,Generator函数执行,返回一个对象,对象拥有next方法。next方法每一次执行,依次迭代一个yield,返回一个新的对象,该对象拥有value、done属性。

3.1 yield后面语句执行时机

1
2
3
4
5
6
7
8
9
10
11
// 2个log哪个先出来
const gen = function*() {
yield setTimeout(() => {
console.log('gen');
}, 1000);
};
// Generator实例 拥有next方法
var genResult = gen();

// next调用执行后,返回一个对象,包含value与done属性
console.log(genResult.next());

知识点一:这里有一个误区,有可能会误认为Generator执行环境在另一个线程中,但实际上不是。yield后面的语句,还是按照正常的环境在执行,yield相当于把该行的语句推出来执行,并且从Generator执行环境中退出来。

再换一个方式,大家再试试

1
2
3
4
5
6
7
8
9
10
let test = 1;
const gen = function*() {
yield test = 2;
test = 3;
};
const result = gen();
console.log(test); // 1
console.log(result.next().value); // 2
// 补充注意:当迭代遇到return、发生错误、或最后一个yield执行完成再执行一遍next后,才会将done变更为true
console.log(result.next().value, test); // undefined 3

3.2 yield的返回值

1
2
3
4
5
6
7
8
9
10
11
12
var gen = function*() {
var b = yield 1 + 2;
console.log('b', b);
};
var result = gen();
console.log('第1次迭代', result.next());
console.log('第2次迭代', result.next());

// 依次出现:
// 第1次迭代 Object {value: 3, done: false}
// b undefined
// 第2次迭代 Object {value: undefined, done: true}

这里或许大家会好奇,为什么b是undefined,而第一次迭代出来的对象,value属性得到了 1+2 的值3。

知识点二:yield语句没有返回值,当 Generator实例调用next()方法后,返回对象value属性,得到了这一次yield 或return 后面语句的求值情况。

结合知识点一,可以把yield看成一种类return的语法。其返回值,只作用在next方法调用后返回的对象的value属性上。而其自身,是没有返回值的。

我们可以试着玩一点改造:把yield 语句换成 yieldFunc,并对后面语句进行一层function包裹

1
2
3
4
5
6
7
const yieldFunc = (() => {
let yieldResult = undefined;
return () => {
yieldResult = 1 + 2;
};
})();
yieldFunc();

如上,类似的,yieldFunc没有返回值,但是yieldResult得到了3。

再让我们把玩一番:将 function* 改造为 generatorFunc,进行一层function包裹。并将每一次yield、return拆分为不同的字符串,用于eval执行。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
const generatorFunc = (arr) => {
let i = 0;
let yieldResult = undefined;
const len = arr.length;

const yieldFunc = (callback) => {
yieldResult = callback();
};

// 用于保留作用域
let evalCall = (i) => {
eval(arr[i]);
evalCall = () => eval(arr[i + 1]);
};

return () => {
return {
next() {
evalCall(i);
i += 1;
return {
value: yieldResult,
done: i === len
}
}
}
};
};

const gen = generatorFunc([`var a = 1; var b = yieldFunc(function () {
return 1 + 2;
})`,
'yieldFunc(function(){console.log("a=" + a + ";b=" + b)});'
]);
const result = gen();

console.log('第1次迭代', result.next());
console.log('第2次迭代', result.next());

// 依次出现
// 第1次迭代 Object {value: 3, done: false}
// a=1;b=undefined
// 第2次迭代 Object {value: undefined, done: true}

是不是发现还挺好玩的?

但是上面的处理,只是最基本的模拟实现,不支持next传递参数(好像说了什么不得了的东西)。

3.3 next传递参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const gen = function*() {
const b = yield 1 + 2;
const c = yield b + '测试';
console.log('c', c);
};
const result = gen();
console.log('第1次迭代', result.next());
console.log('第2次迭代', result.next('第2次迭代'));
console.log('第3次迭代', result.next('第3次迭代'));

// 依次出现:
// 第1次迭代 Object {value: 3, done: false}
// 第2次迭代 Object {value: "第2次迭代测试", done: false}
// c 第3次迭代
// 第3次迭代 Object {value: undefined, done: true}

我们发现,第一次迭代,得到的value 3毫无疑问,是第一次yield出来的求值情况。而yield是没有返回值的,为什么第二次迭代中,b获得了第2次迭代这个字符串,而在第三次迭代中,为什么c获得了第3次迭代这个字符串。

知识点三:next方法可以接受参数,参数替代了上一次yield语句的位置,被作为“返回值”。

Generator总结

记住上面三个知识点,非常重要。

巩固一下最简单的理解(表现层面):把他看成一种新的语法,Generator函数执行,返回一个对象,对象拥有next方法。

  1. next方法每一次执行,依次迭代一个yield(这时候才推出后面的语句,进行执行),返回一个新的对象
  2. yield语句类似于return 本身没有返回值,但其后面语句的求值结果会被作为返回对象的value属性
  3. next方法可以接受参数,参数被当作上一次yield 语句的返回值

下面开始明晰其应用场景

Generator应用场景

至关重要的run函数

1
2
3
4
5
6
7
8
9
10
11
12
13
const run = (fn) => {
const gen = fn();
const next = (data) => {
// 第一次传递的data undefined
const result = gen.next(data);
if (result.done) return;
// 只要保证genrator 内部 yield 出来的东西是函数即可
// yield 出来的是一个函数,接受另一个函数作为参数
// 也就达到了顺序执行的目的
result.value(next);
}
next();
};
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
var getList1 = (success) => {
$.ajax({
url: 'http://sapi.bbeeii.com/martgoods/category/fightgroup.html',
type: 'GET',
dataType: 'jsonp',
jsonpCallback: 'bbeeiiMartgoodsCategoryGet',
success
});
};
var getList2 = (success) => {
$.ajax({
url: 'http://sapi.bbeeii.com/martshow/1-15----1.html',
type: 'GET',
dataType: 'jsonp',
jsonpCallback: 'bbeeiiMartshowGet',
success
});
};
var genFunc = function*() {
// 注意a/b,是通过 上面的data传进来的东西(而不是函数的返回值)
// 写惯了异步,还是非常不习惯的
var a = yield getList1;
var b = yield getList2;
console.log(a, b);
};
run(genFunc);

记住上面提到的三个知识点,能理解a、b值的情况,相信对Generator理解的问题,便不大了。

再让我们加深下理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
run(function * () {
var a = yield (callback) => {
setTimeout(() => {
alert(1);
callback(2);
}, 1000);
}

var b = yield (callback) => {
setTimeout(() => {
console.log(a, b);
var c = a + b;
callback(c);
}, 3000);
}
console.log(a, b);
});

其他

  1. 话糙的最关键点一:yield没有返回值,通过.next传递进来的值,代替上一个yield语句执行的地方;
  2. 话糙的最关键点二:yield出来的东西,作为.next迭代出来的对象的value属性
  3. 概念:不是两个线程,是协程(具体名词还未深究)。但会发现,我们完全用了同步的写法,而底层机制仍然是异步,这是比较精髓的地方。
  4. 当我们把上面的data参数,换成error, data,结合generator.throw,进行try catch等操作,包括yield语句本身可以推出任何东西,这里面的可玩空间,就非常大了。
  5. TJ的co库,应该也是基于上面类似的方式,并且与Promise、thunkify结合,了解run函数,也是从thunkify了解到。
  6. 理解了Generator的概念/机制/应用场景,对ES7 async/await的理解,KOA的使用,相信后续也会方便很多。

参考来源: