ES6(六)Generator函数

本文最后更新于:8 个月前

Generator 函数是如何实现异步编程的?Generator除了作为异步编程的实现方案之外,还有哪些使用场景?Generator与Iterator之间存在着什么关联?Generator的关键字和方法有哪些?

ES6(六)Generator函数

一、简介

Generator 函数是 ES6 提供的一种异步编程解决方案,Generator函数在形式上有两个特征:

  • function关键字与函数名之间有一个星号
  • 函数体内部使用yield表达式,定义不同的内部状态

执行Generator函数返回的是一个遍历器对象。该对象具有两个属性:valuedone,前者表示yeild表达式返回的值,后者表示遍历是否结束。该对象有一个next()方法,必须调用一次它,函数才开始执行,然后执行到遇到yield关键字时停止,但是会将yield关键字右边的表达式执行结果返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* helloWorldGenerator() {
console.log('process 1')
yield 'hello';
console.log('process 2')
yield 'world';
console.log('process 3')
return 'ending';
console.log('process 4')
}

var hw = helloWorldGenerator();
hw.next() // 输出: process 1 返回: {value: 'hello', done: false}
hw.next() // 输出: process 2 返回: {value: 'world', done: false}
hw.next() // 输出: process 3 返回: {value: 'ending', done: true}
hw.next() // 输出: 没有输出 返回: {value: undefined, done: true}

二、yield 表达式

yield 表达式是提供给Genrator函数,用于暂停遍历执行的函数。遍历器对象的next方法的运行逻辑如下:

  • 遇到yield表达式,暂停向下执行,并把yield右边的表达式的值返回返回对象的value的值
  • 下一次调用next(),继续向下执行,直到遇到yield
  • 如果没有遇到新的yield,就一直执行到结束,或者到return为止,将return后面表达式的值作为value返回;如果没有return语句,那value的值就是undefined

注意:yield只能在Generator函数中使用!

另外,yield表达式如果用在另一个表达式之中,必须放在圆括号里面。

1
2
3
4
5
6
7
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError

console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}

yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。

1
2
3
4
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}

三、与Iterator的关系

任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数。

而 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。然后就可以实现一些 Iterable 可遍历对象 才能执行的操作,如:使用扩展操作符、for…of循环遍历

1
2
3
4
5
6
7
8
9
10
11
12
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};

// 注意以下两者的差异
[...myIterable] // [1, 2, 3]
{...myIterable} // {Symbol(Symbol.iterator): ƒ}

for(var x of myIterable) { console.log(x) } // 1 2 3

四、方法

1. next方法

yieldnext()两者结合,可以实现 Generator 函数的双向通信。

yield可以将它右边表达式的值作为返回对象的value属性传递出来,但是yield表达式本身没有返回值。

1
2
3
4
5
6
7
8
// x 不会被赋值,因为yield表达式没有返回值
function* f(){
let x = yield 1
console.log(x)
}
var g1 = f()
g1.next() // 返回:{value: 1, done: false}
g1.next() // 输出:undefined 返回:{value: undefined, done: true}

但是,next()方法可以带一个参数,该参数会被作为上一个yield表达式的返回值

1
2
3
4
5
6
7
function* f(){
let x = yield 1
console.log(x)
}
var g2 = f()
g2.next() // 返回:{value: 1, done: false}
g2.next(2) // 输出:2 返回:{value: undefined, done: true}

2. throw方法

Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获,只能捕获一次,捕获错误后立即结束遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var g = function* () {
try {
yield 1
yield 2
} catch (e) {
console.log('内部捕获', e);
}
};

var i = g();
i.next(); // 返回:{value: 1, done: true}

try {
i.throw('a'); // 输出:内部捕获 a
i.next() // 返回:{value: undefined, done: true}
i.throw('b'); // 输出: 外部捕获 b
} catch (e) {
console.log('外部捕获', e);
}

3. return方法

返回给定的值,并且终结遍历 Generator 函数。

五、yield* 表达式

ES6 提供了yield*表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* foo() {
yield 'a';
yield 'b';
}

function* bar() {
yield 'x';
yield* foo(); // 如果不用yield* 表达式,就需要使用一个for...of循环来遍历执行yield
yield 'y';
}

for (let v of bar()){
console.log(v);
}
// x a b y

六、Generator函数的应用场景

  1. 作为异步编程的解决方案,这个下面将详进一步介绍。
  2. 用来部署Iterator接口,赋值给指定对象的Symbol.iterator属性。任何一个对象的Symbol.iterator属性都是一个遍历器生成器,而Generator函数就是遍历器生成函数。
  3. 作为一种数据结构。更确切地说,可以看作是一个数组结构,因为 Generator 函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口。

七、Generator的异步应用

  • Generator函数的 yield 可以暂停函数的执行
  • yield 表达式可以向函数外传递数据,next()方法可以通过参数向函数内传递数据

利用以上这两点,可以将一个异步任务,同步化。下面是一个手动将异步转为同步的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// fetch是一个异步请求函数
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result);
}

var g = gen()
var result = g.next() // 启动函数的执行,然后执行到 yield fetch(url)这一行停止(fetch被调用)

// result.value 是 fetch()返回的结果,它是一个Promise对象,所以可以用then来指定回调函数
result.value.then(function(data){
return data.json() // 进行json解析
}).then(function(data){
g.next(data) // 通过next()方法将fetch()返回的结果传回函数内部,然后作为yield表达式的值,赋给result
})

分析以上过程,首先是Generator交出执行权给fetch函数,然后fetch函数执行完后,在回调函数中调用next方法,将执行权交还给Generator函数。执行权的交接,就是异步代码转为同步代码的关键步骤。

下面手动模拟一下Generator的执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 异步读取两个文件
var fs = require('fs');
var thunkify = require('thunkify'); // 将多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数的工具
var readFileThunk = thunkify(fs.readFile);

var gen = function* (){
var r1 = yield readFileThunk('/etc/fstab');
console.log(r1.toString());
var r2 = yield readFileThunk('/etc/shells');
console.log(r2.toString());
};

// 模拟过程
var g = gen()
var r1 = gen.next()
r1.value(function(err,data){
if(err) throw err
var r2 = g.next(data)
r2.value(function(err,data){
if(err) throw err
g.next(data)
})
})

但是上面是手动执行的,能不能自动实现执行权的交换(流程管理)呢?当然可以,只需要将回调函数不断传入next方法的value属性就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function run(fn){
var gen = fn()

function next(err,data){
var result = gen.next(data)
if(result.done) return
result.value(next) //不断递归,将回调函数传入next方法的value属性,直到遍历完毕
}

next()
}

function* g(){}
run(g)

ES6(六)Generator函数
http://timegogo.top/2023/03/29/JavaScript/ES6(六)Generator函数/
作者
丘智聪
发布于
2023年3月29日
更新于
2023年7月16日
许可协议