Secrets of the JavaScript Ninja 2nd 読書メモ 第6章 未来のための関数:ジェネレータとプロミス

Secrets of the Javascript Ninja

ジェネレータ(Generators)は特殊な種類の関数である。通常の関数が1つの値しか返さないのに対して、ジェネレータは、実行を断続的に中止しながら、複数の値を生み出すことができる。

プロミス(Promises)は、JavaScriptに組み込みのオブジェクトであり、非同期処理の取り扱いに適している。

ジェネレータ関数

ジェネレータ関数は以下のように定義する。functionキーワードの直後に*(アスタリスク)がついているのがジェネレータ関数の 目印である。ジェネレータ関数の中ではyieldというキーワードを使って値を生成できる。

イテレータオブジェクトでジェネレータを制御する

ジェネレータの呼び出しはジェネレータ関数の本文の実行を意味しない。その代わりに、イテレータオブジェクトが作成される。このオブジェクトを通してジェネレータとコミュニケーションをとることができる。

イテレータはジェネレータの実行をコントロールするために使用される。イテレータオブジェクトはnextメソッドを持っていて、このメソッドを通してジェネレータに値を要求できる。

この呼び出しに対して、ジェネレータはyieldに達するまでコードを実行する。yieldに達すると、その時点での結果を返して、実行を中断する。この結果には、その時点での値(value)と、ジェネレータが完了したか(done)が含まれる。

現在の値の生成が終わると、ジェネレータはそこで中断する。

イテレータの繰り返し

イテレータのnextメソッドを使うことで、ジェネレータに新しい値を要求できる。また、ジェネレータが次の値を持っているか知るには、結果として取得できるオブジェクトのdoneプロパティを使用する。これらを利用すると、古典的なwhileループでイテレータから繰り返し値を取得できる。

for-ofループは、このループのシンタックスシュガーである。

別のジェネレーターを生成する

ジェネレータの実行を別のジェネレータに任せたい場合もある。

yeild*演算子をイテレータに使うことで、別のジェネレータから値を生成できる。

ジェネレータとやりとりする

ジェネレータから値を生み出すだけでなく、ジェネレータに値を送ることもできる。

ジェネレータ関数の引数としてデータを送る

ジェネレータに例外を投げる

ジェネレータの内部を探求する

ジェネレータは小さなプログラムのようなものであり、複数の状態を遷移するステートマシンでもある。

  • 開始前(Suspended start):ジェネレータが作成された直後の状態
  • 実行中(Executing):ジェネレータのコードが実行されている状態
  • 生成停止中(Suspended yield):実行中に、yield式に到達し、処理が中断している状態
  • 完了(Completed):returnに到達するかコードを最後まで実行し、値の生成が完了した状態

ジェネレータの実行コンテキストを追跡する

Promiseとやりとりする

Promiseは、ES2015で導入された、非同期処理を扱いやすくするための概念である。今は存在せず、将来手に入る値のプレースホルダーである。

Promiseに渡したコールバック関数(executor)は即時実行される。

単純なコールバックの問題を理解する

私たちは、長く時間のかかる処理によってユーザの操作を妨げないために、非同期処理を行っている。非同期処理のための最も初歩的な道具はコールバック関数である。

このようなタスクの際にはエラーの発生する可能性がある。コールバック関数の問題は、try-catchのような言語の組み込み機能を使えない点だ。以下のコードではエラーはキャッチできない。

コールバック関数を呼び出すコードはイベントループの同じステップにはないことが普通だたら、例外処理でキャッチはできず、エラーは失われる。

そのため、Node.jsの世界では、コールバック関数にerrとdataの2つの引数が渡されるような慣習が確立されている。errにnullでない値が渡された場合はエラーが発生しているということだ。

コールバック関数の第一の問題点はエラーハンドリングの難しさである。

また、長い処理の後には、取得した結果を使って何か処理を行うことが多い。この結果、別の長い処理が開始されることもある。

このように、連続したステップを実施するのが難しいことが、コールバック関数の第二の問題点である。

さらに、最終的な結果を得るためのステップが、依存関係にない場合もある。このような場合、実行時間を最適化するためには、並行して処理を行うのが望ましい。しかし、並行した処理をコールバック関数で実装するのはかなりトリッキーである(書籍にはコード例があるが、長いので割愛)。コールバック関数の第三の問題点は、並行処理が難しいことである。

コールバック関数は、try-catchやループのような言語の組み込み機能と併せて使うことが難しいのも欠点である。

Promiseを使ってみる

Promiseは非同期に実行するタスクの結果のプレースホルダーである。まだ手に入れておらず、将来入手できる値を意味する。Promiseはいくつかの状態をもつ。

Promiseは、Pending状態から始まる。この状態をUnresolvedと呼ぶこともある。プログラムの実行が進み、Promiseのresolve関数が呼ばれると、PromiseはFullfilled状態に移行する。このとき、望んだ値を手に入れることができる。

一方、Promiseのreject関数が呼ばれるか、例外が発生し、それがPromiseの中でハンドルされなかった場合、PromiseはRejected状態に移行する。この場合、エラーの内容を取得できる。

FullfilledおよびRejectedをまとめてResolved状態とも呼ぶ。一度Resolvedに移行すると、Unresolvedに戻ることはない。

Promiseの却下(reject)

Promiseの実行を失敗させる(rejectする)方法は2つある。明示的に reject メソッドを実行するか、ハンドルされない例外を発生させるかである。

このように、Promiseの中で発生したエラーを一括して処理する仕組みが存在するのがPromiseの大きな利点である。

Promiseの連鎖

then を実行すると新しいPromiseが返る。そのため、 then を繰り返すことでPromiseを連鎖させることができる。

Promiseの連鎖の中で発生したエラーは、末尾の catch で一括でキャッチできる。

複数のPromise

依存関係のない複数の非同期処理を並行して実行することもできる。

Promise.all メソッドはPromiseの配列をとり、全てのPromiseが成功したら返る新しいPromiseを作成する。いずれか1つのPromiseが失敗した場合は、Promiseは失敗する。

いくつかの非同期タスクのうち、最初に完了したものの結果だけを取得したい場合は、 Promsie.raceが利用できる。

ジェネレータとPromiseを組み合わせる

非同期処理を行うコードをジェネレータの中に置くことで、シーケンシャルな非同期処理をエレガントに書くことができる。

async関数

JavaScriptのasync関数は、前述したPromiseとジェネレータの組み合わせを言語機能化したものである。

asyncキーワードを使って非同期関数であることを宣言すると、この中ではawaitキーワードを使って、Promiseがresolveされるのを待つことができる。

コメントを残す