1
0

More than 3 years have passed since last update.

[Node.js] 非同期処理 - ジェネレータ編

Posted at

ジェネレータ

ジェネレータ関数は、処理の途中で停止したり再開したりできる仕組みを持った特殊な関数です。

ジェネレータの生成

function* generatorFunc() {
    yield 1
    yield 2
    yield 3
}
const generator = generatorFunc()

ジェネレータ関数には2つの明確な特徴があります。1つは、functionの後ろに*がつくことで、もう1つはyieldキーワードです。

function* generatorFunc() {
    console.log('ジェネレータ関数開始')
    console.log('yield 1')
    yield 1
    console.log('yield 2')
    yield 2
    console.log('yield 3')
    yield 3
    console.log('ジェネレータ関数終了')
    return 'ジェネレータ関数戻り値'
}
const generator = generatorFunc()
console.log(generator.next())
console.log(generator.next())
console.log(generator.next())
console.log(generator.next())
console.log(generator.next())
console.log(generator.next())
>>>
ジェネレータ関数開始
yield 1
{ value: 1, done: false }
yield 2
{ value: 2, done: false }
yield 3
{ value: 3, done: false }
ジェネレータ関数終了
{ value: 'ジェネレータ関数戻り値', done: true }
{ value: undefined, done: true }
{ value: undefined, done: true }

ジェネレータ関数を実行するとジェネレータを返されますが、この時点ではジェネレータ関数の中の処理は実行されません。生成されたジェネレータのnext()を実行して初めて中の処理が実行されます。実行した後も全て実行されるのではなく、最初のyieldまで実行されて、そこで一時停止します。もう一度実行すると処理が再開されて2つ目のyieldまで実行します。
ジェネレータ関数の処理が完了(上記だと4回目のnext())すると{ value: '値', done: true }(上記だと{ value: 'ジェネレータ関数戻り値', done: true })となります。処理終了後も実行すると{ value: undefined, done: true }が返ってきます。

イテレータとイテラブル

ジェネレータのnext()メソッドの挙動は、イテレータプロトコルという使用に準拠しています。イテレータプロトコルは値の配列を生成するための標準的な方法を定義したもので、この仕様ではnext()メソッドがvalue, doneという2つのプロパティを含むオブジェクトを返します。
ジェネレータは、イテラブルプロトコルという仕様を満たしたイテラブルです。

イテラブルは反復可能なオブジェクトのため、for...of構文で利用可能です。

const generator2 = generatorFunc()
for(const v of generator2) { console.log(v)}
>>>
ジェネレータ関数開始
yield 1
1
yield 2
2
yield 3
3
ジェネレータ関数終了

next()/throw()

- next()に引数を渡して実行

function* resetableGeneratorFunc() {
    let count = 0
    while (true) {
        if (yield count++){
            count = 0
        }
    }
}

const resetableGenerator = resetableGeneratorFunc()
console.log(resetableGenerator.next())
console.log(resetableGenerator.next())
console.log(resetableGenerator.next())
console.log(resetableGenerator.next(true))
console.log(resetableGenerator.next())
console.log(resetableGenerator.next())
>>>
{ value: 0, done: false }
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 0, done: false }
{ value: 1, done: false }
{ value: 2, done: false }

resetableGeneratorFunc()は値をカウントアップする関数ですが、yieldの結果が真に評価された場合、カウンタの値を0にリセットします。結果として、生成されたジェネレータのnext()を真に評価される引数で実行すると、0から値を返します。

  • throw()の使用
function* tryCatchGeneratoreFunc() {
    try {
        yield 1
    } catch (err) {
        console.log('エラーをキャッチ', err)
        yield 2
    }
}

const tryCatchGeneratore = tryCatchGeneratoreFunc()
console.log(tryCatchGeneratore.next())
console.log(tryCatchGeneratore.throw(new Error('エラー')))
console.log(tryCatchGeneratore.next())
>>>
{ value: 1, done: false }
エラーをキャッチ Error: エラー
... (省略)
{ value: 2, done: false }
{ value: undefined, done: true }

エラーがジェネレータ関数ないでキャッチされなかった場合、ジェネレータは終了し、throw()自体もエラーを投げます。

try {
    generatorFunc.throw(new Error('エラー'))
}catch (err) {
    console.log('ジェネレータ外でエラーをキャッチ', err)
}
>>>
ジェネレータ外でエラーをキャッチ TypeError: generatorFunc.throw is not a function
... (省略)

非同期プログラミング

function* asyncWithGeneratorFunc(json) {
    try {
        const result = yield parseJSONAsync(json)
        console.log('パース結果', result)
    } catch (err) {
        console.log('エラーをキャッチ', err)
    }
}
// 正常系
const asyncWithGenerator1 = asyncWithGeneratorFunc('{"foo": 1}')
const promise1 = asyncWithGenerator1.next().value
promise1.then(result => asyncWithGenerator1.next(result))
// 異常系
const asyncWithGenerator2 = asyncWithGeneratorFunc('不正なJSON')
const promise2 = asyncWithGenerator2.next().value
promise2.catch(err => asyncWithGenerator2.throw(err))
>>>
パース結果 { foo: 1 }
エラーをキャッチ SyntaxError: Unexpected token  in JSON at position 0
    at JSON.parse (<anonymous>)
    ...(省略)

異常系の場合にエラーハンドリングを上記にように毎回書くのが手間なので以下のように書き直してみます。

function handleAsyncWithGenerator(generator, resolved) {
    console.log(generator)
    const {done, value} = generator.next(resolved);
    if(done) return Promise.resolve(value)
    return value.then(
        resolved => handleAsyncWithGenerator(generator, resolved),
        err => generator.throw(err)
        );
}
handleAsyncWithGenerator(asyncWithGeneratorFunc('{"foo": 1}'))
handleAsyncWithGenerator(asyncWithGeneratorFunc('不正なJSON'))
>>>
パース結果 { foo: 1 }
エラーをキャッチ SyntaxError: Unexpected token  in JSON at position 0
    at JSON.parse (<anonymous>)
    ...(省略)
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0