## 21.6 生成器实例

本节给出几个例子，用于说明生成器可以用来干什么。

> 下面的 GitHub 仓库包含了示例代码： [generator-examples](https://github.com/rauschma/generator-examples)

### 21.6.1 通过生成器实现迭代器

在关于迭代器的章节，我“顺手”实现了几个迭代器。在本节，我用生成器来实现它们。


#### 21.6.1.1 可迭代的选择器 `take()`

`take()` 将一组可迭代的值（可能是无限的）转换成一个长度 `n` 的序列：

```js
function* take(n, iterable) {
    for (let x of iterable) {
        if (n <= 0) return;
        n--;
        yield x;
    }
}
```

下面是使用它的一个例子：

```js
let arr = ['a', 'b', 'c', 'd'];
for (let x of take(2, arr)) {
    console.log(x);
}
// Output:
// a
// b
```

一个不使用生成器实现的 `take()` 版本会更加复杂：

```js
function take(n, iterable) {
    let iter = iterable[Symbol.iterator]();
    return {
        [Symbol.iterator]() {
            return this;
        },
        next() {
            if (n > 0) {
                n--;
                return iter.next();
            } else {
                maybeCloseIterator(iter);
                return { done: true };
            }
        },
        return() {
            n = 0;
            maybeCloseIterator(iter);
        }
    };
}
function maybeCloseIterator(iterator) {
    if (typeof iterator.return === 'function') {
        iterator.return();
    }
}
```

注意迭代选择器 zip() 通过生成器实现并不会获益多少，因为要使用多个迭代器，不能使用 `for-of` 循环。

#### 21.6.1.2 无限迭代器

`naturalNumbers()` 返回一个迭代所有自然数的迭代器：

```js
function* naturalNumbers() {
    for (let n=0;; n++) {
        yield n;
    }
}
```

这种函数通常和选择器结合使用：

```js
for (let x of take(3, naturalNumbers())) {
    console.log(x);
}
// Output
// 0
// 1
// 2
```

下面是非生成器版本的实现，你可以对比一下：

```js
function naturalNumbers() {
    let n = 0;
    return {
        [Symbol.iterator]() {
            return this;
        },
        next() {
            return { value: n++ };
        }
    }
}
```

#### 21.6.1.3 灵感来自于数组的迭代选择器： `map` ， `filter`

数组可以通过 `map` 和 `filter` 方法转换。这些方法可以一般化为把可迭代对象作为输入，然后输出另一个可迭代对象。

##### 21.6.1.3.1 一般化的 `map()`

下面是 `map()` 的一般化版本：

```js
function* map(iterable, mapFunc) {
    for (let x of iterable) {
        yield mapFunc(x);
    }
}
```

处理无限迭代对象的 `map()` ：

```
> [...take(4, map(naturalNumbers(), x => x * x))]
[ 0, 1, 4, 9 ]
```

##### 21.6.1.3.2 一般化的 `filter()`

下面是 `filter()` 的一般化版本：

```js
function* filter(iterable, filterFunc) {
    for (let x of iterable) {
        if (filterFunc(x)) {
            yield x;
        }
    }
}
```

处理无限迭代对象的 `filter()` ：

```
> [...take(4, filter(naturalNumbers(), x => (x % 2) === 0))]
[ 0, 2, 4, 6 ]
```

### 21.6.2 用于延迟执行的生成器

接下来的两个例子展示生成器如何用于处理字符流。

* 输入是字符流。
* 第一步 - 分词（字符→单词）：字符组合成单词，就是满足正则表达式 `/^[A-Za-z0-9]$/` 的字符串。忽略非单词的字符，但是这些字符分割单词。这一步的输入是字符流，输出是单词流。
* 第二步 - 提取数字（单词→数字）：仅保留匹配正则 `/^[0-9]+$/` 的单词，并将它们转换成数字。
* 第三步 - 添加数字（数字→数字）：对于每个接收到的数字，返回到目前为止接收到的所有数字之和。

所有内容都延迟执行（增量地和按需地），很平滑优雅：接收到第一个字符之后就立即开始计算。例如，我们不需要等到接受完所有字符才去拿第一个单词。

#### 21.6.2.1 延迟拉取（用作迭代器的生成器）

用生成器实现的延迟拉取以如下的方式工作。实现步骤一到三的三个生成器像下面这样链式调用：

```
addNumbers(extractNumbers(tokenize(CHARS)))
```

链中的每个成员从数据源中拉取数据，然后生产出另一组数据。整个过程开始于 `tokenize` ，它的数据源就是字符串 CHARS 。

##### 21.6.2.1.1 第一步 - 分词

下面的技巧使得代码更简单一点儿：迭代器的最后一个结果（属性 `done` 是 `false` ）设置为结束标记 END_OF_SEQUENCE 。

```js
/**
 * Returns an iterable that transforms the input sequence
 * of characters into an output sequence of words.
 */
function* tokenize(chars) {
    let iterator = chars[Symbol.iterator]();
    let ch;
    do {
        ch = getNextItem(iterator); // (A)
        if (isWordChar(ch)) {
            let word = '';
            do {
                word += ch;
                ch = getNextItem(iterator); // (B)
            } while (isWordChar(ch));
            yield word; // (C)
        }
        // Ignore all other characters
    } while (ch !== END_OF_SEQUENCE);
}
const END_OF_SEQUENCE = Symbol();
function getNextItem(iterator) {
    let {value,done} = iterator.next();
    return done ? END_OF_SEQUENCE : value;
}
function isWordChar(ch) {
    return typeof ch === 'string' && /^[A-Za-z0-9]$/.test(ch);
}
```

该生成器是如何延迟的？当通过 `next()` 请求一个值的时候，该生成器就从 `iterator` （行 A 和行 B ）中拉取数据，处理之后就放在 yield 后面返回出去（行 C ）。然后生成器函数暂停，直到下一次数据请求的到来。这意味着这种分词在第一个字符就绪的时候就开始了，这对流（ stream ）很方便。

让我们尝试一下分词。注意空格和点符号不是单词，忽略它们，但是它们分割单词。我们利用一个事实：字符串就是字符（ unicode 码点）的迭代。 `tokenize()` 的结果就是一组单词的迭代，可以通过扩展操作符（...）将其转换为数组。

```
> [...tokenize('2 apples and 5 oranges.')]
[ '2', 'apples', 'and', '5', 'oranges' ]
```

##### 21.6.2.1.2 第二步 - 提取数字

这一步相当简单，仅将包含数字的单词（通过 `Number()` 转换成数字类型）放在 `yield` 之后。

```js
/**
 * Returns an iterable that filters the input sequence
 * of words and only yields those that are numbers.
 */
function* extractNumbers(words) {
    for (let word of words) {
        if (/^[0-9]+$/.test(word)) {
            yield Number(word);
        }
    }
}
```

可以再次看到延迟：如果通过 `next()` 请求一个数字，那么在 `words` 中遇到一个数字单词的时候，就会立马获取到（通过 `yield` ）这个数字。

让我们从一组单词中获取数字：

```
> [...extractNumbers(['hello', '123', 'world', '45'])]
[ 123, 45 ]
```

注意字符串转换成了数字。

##### 21.6.2.1.3 第三步 - 数字相加

```js
/**
 * Returns an iterable that contains, for each number in
 * `numbers`, the total sum of numbers encountered so far.
 * For example: 7, 4, -1 --> 7, 11, 10
 */
function* addNumbers(numbers) {
    let result = 0;
    for (let n of numbers) {
        result += n;
        yield result;
    }
}
```

尝试一个简单的例子：

```
> [...addNumbers([5, -2, 12])]
[ 5, 3, 15 ]
```

##### 21.6.2.1.4 获取输出

生成器链本身并不会产生输出。我们需要通过扩展操作符主动地获得输出数据：

```js
const CHARS = '2 apples and 5 oranges.';
const CHAIN = addNumbers(extractNumbers(tokenize(CHARS)));
console.log([...CHAIN]);
    // [ 2, 7 ]
```

辅助函数 `logAndYield` 让我们能够判断出是否真的延迟执行了：

```js
function* logAndYield(iterable, prefix='') {
    for (let item of iterable) {
        console.log(prefix + item);
        yield item;
    }
}

const CHAIN2 = logAndYield(addNumbers(extractNumbers(tokenize(logAndYield(CHARS)\
))), '-> ');
[...CHAIN2];

// Output:
// 2
//  
// -> 2
// a
// p
// p
// l
// e
// s
//  
// a
// n
// d
//  
// 5
//  
// -> 7
// o
// r
// a
// n
// g
// e
// s
// .
```

上面的输出表明 `addNumbers` 在接收到 `'2'` 和 `''` 的时候就生成了一个结果。

#### 21.6.2.2 延迟推入数据（用作观察者的生成器）

将前面基于输出的逻辑改成一个基于输入的例子不需要做太多事情。步骤是一样的。但是不是以获取数据而结束，而是以输入数据而开始。

正如之前讲解的，如果生成器通过 `yield` 接收到输入，那么该生成器上的第一次 `next()` 调用什么也不干。这就是为什么我要使用之前展示的辅助函数 `coroutine()` 来在此处创建协同程序，它帮助我们首次调用 `next()` 。

下面的函数 `send()` 完成数据推入。

```js
/**
 * Pushes the items of `iterable` into `sink`, a generator.
 * It uses the generator method `next()` to do so.
 */
function send(iterable, sink) {
    for (let x of iterable) {
        sink.next(x);
    }
    sink.return(); // signal end of stream
}
```

当生成器处理流的时候，它需要知道流的结束点，以便于正确地做清理工作。对于拉取数据，我们通过一个特殊的流终结符来做这件事情。对于推入数据，流的结束信号通过 `return()` 发出。

让我们通过一个生成器来测试 `send()` ，该生成器只是简单地打印出接收到的数据：

```js
/**
 * This generator logs everything that it receives via `next()`.
 */
const logItems = coroutine(function* () {
    try {
        while (true) {
            let item = yield; // receive item via `next()`
            console.log(item);
        }
    } finally {
        console.log('DONE');
    }
});
```

通过一个字符串（可以迭代其中的 unicode 码点）给 `logItems()` 传入三个字符。

```
> send('abc', logItems());
a
b
c
DONE
```

##### 21.6.2.2.1 第一步 - 分词

注意生成器是如何在两个 `finally` 子句中处理流结束的（通过 `return()` 发出信号）。我们依赖于 `return()` 信号被送给两个 `yield` 中的一个。否则，该生成器将永不会结束，因为从行 A 开始的无限循环不会停止。

```js
/**
 * Receives a sequence of characters (via the generator object
 * method `next()`), groups them into words and pushes them
 * into the generator `sink`.
 */
const tokenize = coroutine(function* (sink) {
    try {
        while (true) { // (A)
            let ch = yield; // (B)
            if (isWordChar(ch)) {
                // A word has started
                let word = '';
                try {
                    do {
                        word += ch;
                        ch = yield; // (C)
                    } while (isWordChar(ch));
                } finally {
                    // The word is finished.
                    // We get here if
                    // - the loop terminates normally
                    // - the loop is terminated via `return()` in line C
                    sink.next(word); // (D)
                }
            }
            // Ignore all other characters
        }
    } finally {
        // We only get here if the infinite loop is terminated
        // via `return()` (in line B or C).
        // Forward `return()` to `sink` so that it is also
        // aware of the end of stream.
        sink.return();
    }
});

function isWordChar(ch) {
    return /^[A-Za-z0-9]$/.test(ch);
}
```

这一次，该延迟通过数据推入来驱动：当生成器接收到能够构成一个单词的字符数时（在行 C ），就会把该单词推入 `sink` （行 D ）。也就是说，生成器不会等到接收到所有的字符才开始工作。

`tokenize()` 显示了生成器作为线性状态机的实现，工作得很好。在此场景下，状态机有两种状态：“在单词内部”和“没在单词内部”。

让我们对一个字符串分词：

```
> send('2 apples and 5 oranges.', tokenize(logItems()));
2
apples
and
5
oranges
```

##### 21.6.2.2.2 第二步 - 获取数字

这一步很直接。

```js
/**
 * Receives a sequence of strings (via the generator object
 * method `next()`) and pushes only those strings to the generator
 * `sink` that are “numbers” (consist only of decimal digits).
 */
const extractNumbers = coroutine(function* (sink) {
    try {
        while (true) {
            let word = yield;
            if (/^[0-9]+$/.test(word)) {
                sink.next(Number(word));
            }
        }
    } finally {
        // Only reached via `return()`, forward.
        sink.return();
    }
});
```

同样是延迟的：当遇到数字的时候，就推入 `sink` 。

让我们从一组单词中获取数字：

```
> send(['hello', '123', 'world', '45'], extractNumbers(logItems()));
123
45
DONE
```

注意输入是一个字符串序列，输出是一个数字序列。

##### 21.6.2.2.3 第三步 - 数字求和

这一次，通过推入单个值来响应流的结束，然后关闭 sink 。

```js
/**
 * Receives a sequence of numbers (via the generator object
 * method `next()`). For each number, it pushes the total sum
 * so far to the generator `sink`.
 */
const addNumbers = coroutine(function* (sink) {
    let sum = 0;
    try {
        while (true) {
            sum += yield;
            sink.next(sum);
        }
    } finally {
        // We received an end-of-stream
        sink.return(); // signal end of stream
    }
});
```

让我们试一下这个生成器：

```
> send([5, -2, 12], addNumbers(logItems()));
5
3
15
DONE
```

##### 21.6.2.2.4 推入输入数据

生成器链以 `tokenize` 开始，以 `logItems` 结束，并且打印出所有接收到的东西。我们通过 `send` 推入一系列字符到生成器链：

```js
const INPUT = '2 apples and 5 oranges.';
const CHAIN = tokenize(extractNumbers(addNumbers(logItems())));
send(INPUT, CHAIN);

// Output
// 2
// 7
// DONE
```

下面的代码证明整个处理过程真的是延迟发生的：

```js
const CHAIN2 = tokenize(extractNumbers(addNumbers(logItems({ prefix: '-> ' }))));
send(INPUT, CHAIN2, { log: true });

// Output
// 2
//  
// -> 2
// a
// p
// p
// l
// e
// s
//  
// a
// n
// d
//  
// 5
//  
// -> 7
// o
// r
// a
// n
// g
// e
// s
// .
// DONE
```

打印出来的内容显示 `addNumbers` 在接收到 `'2'` 和 `''` 的时候就立马产生一个结果。

### 21.6.3 通过生成器实现的多任务协作

#### 21.6.3.1 推入长时间运行的任务

在本例中，我们创建一个在 web 页面显示的计数器。我们不断地优化最初的版本，直到得到一个多任务协作的版本，并且不会阻塞主线程和用户界面。

下面是 web 页面的一部分，在其中展示计数器：

```html
<body>
    Counter: <span id="counter"></span>
</body>
```

下面的函数不停地计数：

```js
function countUp(start = 0) {
    const counterSpan = document.querySelector('#counter');
    while (true) {
        counterSpan.textContent = String(start);
        start++;
    }
}
```

如果你运行这个函数，它会完全阻塞它所运行在的用户界面线程，并且失去响应。

让我们用生成器实现同样的功能，该生成器通过 `yield` 定期暂停（后面会展示一个用于运行该生成器的调度函数）：

```js
function* countUp(start = 0) {
    const counterSpan = document.querySelector('#counter');
    while (true) {
        counterSpan.textContent = String(start);
        start++;
        yield; // pause
    }
}
```

让我们稍微地改进一下。把用户界面的更新挪到另一个生成器 `displayCounter` 里面，通过 `yield*` 调用该生成器。由于是一个生成器函数，所以也能够暂停。

```js
function* countUp(start = 0) {
    while (true) {
        start++;
        yield* displayCounter(start);
    }
}
function* displayCounter(counter) {
    const counterSpan = document.querySelector('#counter');
    counterSpan.textContent = String(counter);
    yield; // pause
}
```

最后，下面是一个调度函数，可以用它来调用 `countUp()` 。生成器执行的每一步都由一个独立的任务处理，该任务通过 `setTimeout()` 创建。这意味着用户界面可以调度其它任务，并且保持可响应。

```js
function run(generatorObject) {
    if (!generatorObject.next().done) {
        // Add a new task to the event queue
        setTimeout(function () {
            run(generatorObject);
        }, 1000);
    }
}
```

有了 `run` 的帮助，我们得到了一个近乎无尽的计数器，并且不会阻塞用户界面：

```js
run(countUp());
```

你可以[在线运行这个例子](https://rauschma.github.io/generator-examples/nonblocking-counter/)。

#### 21.6.3.2 生成器版本的多任务协作和 Node.js 风格的回调

如果你调用一个生成器函数（或者方法），该函数（或方法）并不能访问到它的生成器对象；它的 `this` 与它是普通函数时的 `this` 一样。一种变通方法就是通过 `yield` 将生成器对象传入该生成器。

下面的 Node.js 脚本使用了这种技术，但是将生成器包裹在一个回调中（ `next` ，行 A ）。必须要通过 [babel-node](https://leanpub.com/exploring-es6/read#sec_babel-node) 运行。

```js
import {readFile} from 'fs';

let fileNames = process.argv.slice(2);

run(function* () {
    let next = yield;
    for (let f of fileNames) {
        let contents = yield readFile(f, { encoding: 'utf8' }, next);
        console.log('##### ' + f);
        console.log(contents);
    }
});
```

在行 A ，我们使用一个 Node.js 回调风格的的回调函数。该回调函数使用生成器对象唤醒生成器，你可以看一下 `run()` 的实现代码：

```js
function run(generatorFunction) {
    let generatorObject = generatorFunction();

    // Step 1: Proceed to first `yield`
    generatorObject.next();

    // Step 2: Pass in a function that the generator can use as a callback
    function nextFunction(error, result) { // (A)
        if (error) {
            generatorObject.throw(error);
        } else {
            generatorObject.next(result);
        }
    }
    generatorObject.next(nextFunction);

    // Subsequent invocations of `next()` are triggered by `nextFunction`
}
```

#### 21.6.3.3 通信顺序进程（ CSP ）

[js-csp](https://github.com/ubolonton/js-csp) 将通信顺序进程（ CSP ）引入了 JavaScript 。 CSP 是一种多任务协作的方式，类似于 ClojureScript 的 core.async 和 Go 的 goroutine 。 `js-csp` 有两处抽象：

* 进程：是多任务协作的，通过将生成器函数传递给调度函数 `go()` 来实现。
* 通道：用于进程通信的队列。通过调用 `chan()` 创建通道。

作为一个示例，让我们使用 CSP 处理 DOM 事件，这是一种让人联想到函数式编程的方式。下面的代码使用函数 `listen()` （将在后面展示）创建一个输出 `mousemove` 事件的通道，然后在一个无限循环中通过 `take` 持续地获得输出内容。多亏了 `yield` ，进程才会阻塞直到通道有输出。

```js
import csp from 'js-csp';

csp.go(function* () {
    let element = document.querySelector('#uiElement1');
    let channel = listen(element, 'mousemove');
    while (true) {
        let event = yield csp.take(channel);
        let x = event.layerX || event.clientX;
        let y = event.layerY || event.clientY;
        element.textContent = `${x}, ${y}`;
    }
});
```

`listen()` 的实现如下所示：

```js
function listen(element, type) {
    let channel = csp.chan();
    element.addEventListener(type,
        event => {
            csp.putAsync(channel, event);
        });
    return channel;
}
```

> 此例来自 James Long 的博客[《 Taming the Asynchronous Beast with CSP Channels in JavaScript 》](http://jlongster.com/Taming-the-Asynchronous-Beast-with-CSP-in-JavaScript) 。查阅这篇博客以获得更多关于 CSP 的信息。





