21.4 用作观察者的生成器(数据消费)
作为数据消费者,生成器对象遵循生成器接口的后半部分, Observer
:
interface Observer {
next(value? : any) : void;
return(value? : any) : void;
throw(error) : void;
}
作为一个观察者,生成器处于暂停状态,直到有输入进来。有三种类型的输入,通过接口中声明的方法来传入数据:
next()
传入正常的输入数据。return()
终止生成器。throw()
发出一个出错信号。
21.4.1 通过 next()
发送值
如果将生成器用作观察者,那么可以通过 next()
传入值,然后通过 yield
获取传入的值。
function* dataConsumer() {
console.log('Started');
console.log(`1. ${yield}`); // (A)
console.log(`2. ${yield}`);
return 'result';
}
首先,创建一个生成器对象:
> let genObj = dataConsumer();
现在调用 genObj.next()
,启动生成器,执行到第一个 yield 处,然后暂停。 next()
的返回值就是在行 A yield 后面的值(是 undefined
,因为 yield
没有操作数)。在本节,我们对 next()
的返回值不感兴趣,因为仅使用 next()
传送值,而不是获取值。
> genObj.next()
Started
{ value: undefined, done: false }
为了将值 a
传给第一个 yield ,值 b
传给第二个 yield ,再调用两次 next()
:
> genObj.next('a')
1. a
{ value: undefined, done: false }
> genObj.next('b')
2. b
{ value: 'result', done: true }
最后一个 next()
的返回值就是 dataConsumer()
中 return 返回的值。 done
为 true
表明生成器执行完成了。
很不幸, next()
是非对称的:它总是传值给当前暂停的 yield ,返回下一个 yield 的操作数。
21.4.1.1 第一个 next()
在把生成器当成观察者的时候,一定要注意第一次 next()
调用是为了启动观察者,然后才接收输入,因为第一次调用使执行流程推进到了第一个 yield
处。因此,不能通过第一个 next()
传送值 - 如果这样做了,甚至会得到一个错误:
> function* g() { yield }
> g().next('hello')
TypeError: attempt to send 'hello' to newborn generator
下面的工具函数修复了这个问题:
/**
* Returns a function that, when called,
* returns a generator object that is immediately
* ready for input via `next()`
*/
function coroutine(generatorFunction) {
return function (...args) {
let generatorObject = generatorFunction(...args);
generatorObject.next();
return generatorObject;
};
}
为了理解 coroutine()
是如何工作的,让我们比较一下包装了的生成器和普通的生成器:
const wrapped = coroutine(function* () {
console.log(`First input: ${yield}`);
return 'DONE';
});
const normal = function* () {
console.log(`First input: ${yield}`);
return 'DONE';
};
包装了的生成器立刻就可以接收输入了:
> wrapped().next('hello!')
First input: hello!
普通的生成器需要额外调用一次 next()
才能接收输入:
> let genObj = normal();
> genObj.next()
{ value: undefined, done: false }
> genObj.next('hello!')
First input: hello!
{ value: 'DONE', done: true }
21.4.2 yield
的结合性很低
yield
的结合性很低,因此没必要将它的操作数放在括号里面:
yield a + b + c;
这被看作:
yield (a + b + c);
而不是:
(yield a) + b + c;
很多操作符的结合性都比 yield
高,如果想把整个 yield
表达式用作操作数,就必须将其放在括号里面。例如,如果用不带括号的 yield
表达式作为加法运算的操作数,就会抛出语法错误:
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
如果 yield
表达式直接就是函数或者方法的参数,可以不用括号:
foo(yield 'a', yield 'b');
如果用作赋值的右操作数,也可以不用括号:
let input = yield;
21.4.2.1 ES6 语法中的 yield
从下面的来自于 ECMAScript 6 规范的语法规则中可以看出用括号包裹 yield
是否有必要。这些规则描述了如何解析表达式。我按照一般性(底结合性,底优先级)到特殊性(高结合性,高优先级)的顺序列出这些规则。在任何需要使用某种表达式的地方,也可以使用该表达式更特殊的版本。反过来就不行了。下面的层级结构以 ParenthesizedExpression
结束,这意味着如果将 yield
表达式放在括号里面,就可以在任何表达式的任何位置正常使用了。
Expression :
AssignmentExpression
Expression , AssignmentExpression
AssignmentExpression :
ConditionalExpression
YieldExpression
ArrowFunction
LeftHandSideExpression = AssignmentExpression
LeftHandSideExpression AssignmentOperator AssignmentExpression
···
AdditiveExpression :
MultiplicativeExpression
AdditiveExpression + MultiplicativeExpression
AdditiveExpression - MultiplicativeExpression
MultiplicativeExpression :
UnaryExpression
MultiplicativeExpression MultiplicativeOperator UnaryExpression
···
PrimaryExpression :
this
IdentifierReference
Literal
ArrayLiteral
ObjectLiteral
FunctionExpression
ClassExpression
GeneratorExpression
RegularExpressionLiteral
TemplateLiteral
ParenthesizedExpression
ParenthesizedExpression :
( Expression )
AdditiveExpression
的操作数是 AdditiveExpression
和 MultiplicativeExpression
。因此,使用(更特殊的) ParenthesizedExpression
作为操作数是可以的,但是使用(更一般的) YieldExpression
就不行了。
21.4.3 return()
和 throw()
让我们回顾一下 next(x)
是如何工作的(在第一次调用之后):
- 1、生成器此时暂停在一个
yield
操作符处。 - 2、传递值
x
给上面的yield
,这意味着yield
表达式的值就为x
。 - 3、执行到下一个
yield
或者return
:yield x
使next()
返回{ value: x, done: false }
return x
使next()
返回{ value: x, done: true }
return()
和 throw()
类似于 next()
,但是在第二步中做了不一样的事情:
return(x)
在yield
处执行return x
。throw(x)
在yield
处执行throw x
。
21.4.4 return()
终止生成器
return()
在 yield
处执行 return
,使该处 yield
成为生成器的最后一次暂停点。让我们使用下面的生成器函数来看看这是怎么工作的。
function* genFunc1() {
try {
console.log('Started');
yield; // (A)
} finally {
console.log('Exiting');
}
}
在下面的交互执行过程中,首先使用 next()
启动生成器,执行到行 A 的 yield 处暂停。然后在那个位置通过 return()
返回。
> let genObj1 = genFunc1();
> genObj1.next()
Started
{ value: undefined, done: false }
> genObj1.return('Result')
Exiting
{ value: 'Result', done: true }
21.4.4.1 组织终止
如果在 finally
子句中使用 yield ,则可以阻止 return()
终止生成器(也可以在 finally
子句中使用 return
语句来阻止)。
function* genFunc2() {
try {
console.log('Started');
yield;
} finally {
yield 'Not done, yet!';
}
}
这次, return()
并不会退出生成器函数。相应地,它返回的对象的 done
属性为 false
。
> let genObj2 = genFunc2();
> genObj2.next()
Started
{ value: undefined, done: false }
> genObj2.return('Result')
{ value: 'Not done, yet!', done: false }
可以再调用一次 next()
。类似于非生成器函数,生成器函数的返回值是在进入 finally
子句之前放入队列中的值。
> genObj2.next()
{ value: 'Result', done: true }
21.4.4.2 从新生的生成器返回
从新生(尚未启动)的生成器中返回值是可以的:
> function* genFunc() {}
> genFunc().return('yes')
{ value: 'yes', done: true }
深入阅读
关于迭代的那一章中有一节详细讲解了关闭迭代器和生成器。
21.4.5 throw()
抛出错误
throw()
在 yield
处抛出异常,使该 yield
成为生成器最后的暂停点。让我们通过下面的生成器函数看看这是如何工作的。
function* genFunc1() {
try {
console.log('Started');
yield; // (A)
} catch (error) {
console.log('Caught: ' + error);
}
}
在下面的交互过程中,首先调用 next()
启动生成器,执行到行 A 的 yield
处暂停,然后在那个位置抛出异常。
> let genObj1 = genFunc1();
> genObj1.next()
Started
{ value: undefined, done: false }
> genObj1.throw(new Error('Problem!'))
Caught: Error: Problem!
{ value: undefined, done: true }
throw()
(最后一行展示)的返回值来自于离开生成器函数时隐式的 return
。
21.4.5.1 未捕获的异常
如果没有在生成器内部捕获异常,就会通过 throw()
抛出来。例如,下面的生成器函数不捕获异常:
function* genFunc2() {
console.log('Started');
yield; // (A)
}
如果在行 A 处用 throw()
抛出一个 Error
的实例,那么该生成器函数自身就会抛出这个错误:
> let genObj2 = genFunc2();
> genObj2.next()
Started
{ value: undefined, done: false }
> genObj2.throw(new Error('Problem!'))
Error: Problem!
21.4.5.2 从新生的生成器抛出异常
在一个新生(尚未启动)的生成器中抛出异常是可以的:
> function* genFunc() {}
> genFunc().throw(new Error('Problem!'))
Error: Problem!
21.4.6 示例:处理异步推送的数据
作为观察者的生成器在等待输入的时候暂停的特性使其能非常完美地按需处理异步数据。可以创建一个生成器链来处理这些异步的数据,下面描述了生成器链的要素:
- 生成器链中的每一个成员(除了最后一个)都有一个
target
参数。这些成员通过yield
获取数据,通过target.next()
发送数据。 - 生成器链的最后一个成员没有
target
参数,仅获取数据。
整个生成器前面有一个非生成器函数,该函数发出异步请求,并将请求结果通过 next()
推送给生成器链。
作为一个例子,让我们将一组生成器组成一条链来处理异步读取的文件数据。
本例中的代码位于文件 generator-examples/node/readlines.js 中。必须通过
babel-node
执行。
下面的代码创建这条链:它包含生成器 splitLines
, numberLines
和 printLines
。数据通过非生成器函数 readFile()
推送给生成器链。
readFile(fileName, splitLines(numberLines(printLines())));
在我展示这些函数的代码的时候,会讲解它们都做了些什么。
正如之前讲解的,如果生成器通过 yield
接收输入数据,生成器对象上的第一次 next()
调用不会做任何事。这就是为什么我要使用之前展示的辅助方法 coroutine()
来创建协同程序。该辅助方法帮助我们执行第一次的 next()
调用。
readFile()
是一个非生成器函数,启动了整个流程:
import {createReadStream} from 'fs';
/**
* Create an asynchronous ReadStream for the file whose name
* is `fileName` and feed it to the generator object `target`.
*
* @see ReadStream https://nodejs.org/api/fs.html#fs_class_fs_readstream
*/
function readFile(fileName, target) {
let readStream = createReadStream(fileName,
{ encoding: 'utf8', bufferSize: 1024 });
readStream.on('data', buffer => {
let str = buffer.toString('utf8');
target.next(str);
});
readStream.on('end', () => {
// Signal end of output sequence
target.return();
});
}
生成器链以 splitLines
开始:
/**
* Turns a sequence of text chunks into a sequence of lines
* (where lines are separated by newlines)
*/
const splitLines = coroutine(function* (target) {
let previous = '';
try {
while (true) {
previous += yield;
let eolIndex;
while ((eolIndex = previous.indexOf('\n')) >= 0) {
let line = previous.slice(0, eolIndex);
target.next(line);
previous = previous.slice(eolIndex+1);
}
}
} finally {
// Handle the end of the input sequence
// (signaled via `return()`)
if (previous.length > 0) {
target.next(previous);
}
// Signal end of output sequence
target.return();
}
});
请注意:
readFile()
使用生成器对象方法return()
来通知生成器链它发送的数据块结束了。- 在
splitLines
的无限循环里通过yield
等待输入数据的时候,readFile()
发出数据结束信号。return()
使其跳出那个无限循环。 splitLines
使用finally
子句处理扫尾工作。
下一个生成器是 numberLines
:
/**
* Prefixes numbers to a sequence of lines
*/
const numberLines = coroutine(function* (target) {
try {
for (let lineNo = 0; ; lineNo++) {
let line = yield;
target.next(`${lineNo}: ${line}`);
}
} finally {
// Signal end of output sequence
target.return();
}
});
最后一个生成器是 printLines
:
/**
* Receives a sequence of lines (without newlines)
* and logs them (adding newlines).
*/
const printLines = coroutine(function* () {
while (true) {
let line = yield;
console.log(line);
}
});
上述代码很奇妙,所有的事情都是惰性的(按需的):在数据到达的时候,拆分行,加上行号,打印出来;在能够打印之前并不需要等到所有文本都读取完毕。
21.4.7 yield* : 所有细节
yield*
可以实现从一个生成器函数中(调用者)调用另一个生成器函数(被调用者)的功能。
到目前为止,我们仅看到了 yield
的一个方面:将 yield
中的值从被调用者传给调用者。既然我们对生成器接收输入感兴趣,那么另一方面就变得有意义了: yield*
也将调用者接收到的输入数据转发给被调用者。
首先我将展示如何在 JavaScript 中实现 yield*
,以此来讲解 yield*
的完整语义。然后给出简单的例子,调用者通过 next()
、 return()
和 throw()
接收到输入数据,转发给被调用者。
下面的语句:
let yieldStarResult = yield* calleeFunc();
大致等价于:
let yieldStarResult;
let calleeObj = calleeFunc();
let prevReceived = undefined;
while (true) {
try {
// Forward input previously received
let {value,done} = calleeObj.next(prevReceived);
if (done) {
yieldStarResult = value;
break;
}
prevReceived = yield value;
} catch (e) {
// Pretend `return` can be caught like an exception
if (e instanceof Return) {
// Forward input received via return()
calleeObj.return(e.returnedValue);
return e.returnedValue; // “re-throw”
} else {
// Forward input received via throw()
calleeObj.throw(e); // may throw
}
}
}
为了保持简单,上面的代码没有考虑几件事:
yield*
的操作数可以是任何可迭代的值。return()
和throw()
是可选的迭代器方法。应该仅在它们存在的时候才调用。- 如果接收到异常,
throw()
不存在,但是有return()
,此时调用return()
(在抛出异常之前),让calleeObject
有机会做清理工作。 calleeObj
可以通过返回一个done
属性为false
的对象来拒绝关闭。然后调用者也不得不拒绝关闭,yield*
继续迭代。
21.4.7.1 示例:用 yield*
转发 next()
传入的值
下面的生成器函数 caller()
通过 yield*
调用生成器函数 callee()
。
function* callee() {
console.log('callee: ' + (yield));
}
function* caller() {
while (true) {
yield* callee();
}
}
callee
打印 next()
传入的值,这样就可以检查传给 caller
的值是 a
还是 b
。
> let callerObj = caller();
> callerObj.next() // start
{ value: undefined, done: false }
> callerObj.next('a')
callee: a
{ value: undefined, done: false }
> callerObj.next('b')
callee: b
{ value: undefined, done: false }
21.4.7.2 示例:用 yield*
转发 throw()
抛出的异常
用下面一段代码来展示当 yield*
代理另一个生成器的时候, throw()
是如何工作的。
function* callee() {
try {
yield 'b'; // (A)
yield 'c';
} finally {
console.log('finally callee');
}
}
function* caller() {
try {
yield 'a';
yield* callee();
yield 'd';
} catch (e) {
console.log('[caller] ' + e);
}
}
首先创建一个生成器对象,然后执行到行 A 。
> let genObj = caller();
> genObj.next().value
'a'
> genObj.next().value
'b'
在行 A ,抛出一个异常:
> genObj.throw(new Error('Problem!'))
finally callee
[caller] Error: Problem!
{ value: undefined, done: true }
callee
并不会捕获该异常,异常会传进 caller
,所以会在 caller
执行完之前打印出异常。
21.4.7.3 用 yield*
转发 return()
返回值
用下面一段代码来展示当 yield*
代理另一个生成器的时候, return()
是如何工作的。
function* callee() {
try {
yield 'b';
yield 'c';
} finally {
console.log('finally callee');
}
}
function* caller() {
try {
yield 'a';
yield* callee();
yield 'd';
} finally {
console.log('finally caller');
}
}
解构在拿到了需要的元素之后,如果迭代器还未迭代完,那么就会通过 return()
关闭这个迭代器:
let [x, y] = caller(); // ['a', 'b']
// Output:
// finally callee
// finally caller
有趣的是, return()
被送入 caller
,然后转发给 callee
( callee
更早结束 ),但是之后也会终止 caller
(这是调用 return()
的人所希望看到的)。也就是说, return
的传播和异常很类似。
理解
return()
的小提示为了理解
return()
,这样问自己是很有帮助的:如果我把 callee 函数中代码复制粘贴到 caller 函数中,会发生什么。