Promiseは then
や catch
等のメソッドを繋げて書いていきます。
これはDOMやjQuery等でよくみられるメソッドチェーンとよく似ています。
一般的なメソッドチェーンは this
を返すことで、メソッドを繋げて書けるようになっています。
Note
|
メソッドチェーンの作り方については メソッドチェーンの作り方 - あと味 などを参照するといいでしょう。 |
一方、Promiseは毎回新しいpromiseオブジェクトを返すようになっていますが、 一般的なメソッドチェーンと見た目は全く同じです。
このセクションでは、一般的なメソッドチェーンで書かれたものを インターフェースはそのままで内部的にはPromiseで処理されるようにする方法について学んでいきたいと思います。
以下のような Node.jsのfsモジュールを例にしてみたいと思います。
また、今回の例は見た目のわかりやすさを重視しているため、 現実的にはあまり有用なケースとはいえないかもしれません。
link:src/promise-chain/fs-method-chain.js[role=include]
このモジュールは以下のようにread → transform → writeという流れを メソッドチェーンで表現することができます。
const File = require("./fs-method-chain");
const inputFilePath = "input.txt";
const outputFilePath = "output.txt";
File.read(inputFilePath)
.transform((content) => {
return ">>" + content;
})
.write(outputFilePath);
transform
は引数で受け取った値を変更する関数を渡して処理するメソッドです。
この場合は、readで読み込んだ内容の先頭に >>
という文字列を追加しているだけです。
次に先ほどのメソッドチェーンをインターフェースはそのまま維持して 内部的にPromiseを使った処理にしてみたいと思います。
link:src/promise-chain/fs-promise-chain.js[role=include]
内部に持ってるpromiseオブジェクトに対するエイリアスとして
then
と catch
を持たせていますが、それ以外のインターフェースは全く同じ使い方となっています。
そのため、先ほどのコードで require
するモジュールを変更しただけで動作します。
const File = require("./fs-promise-chain");
const inputFilePath = "input.txt";
const outputFilePath = "output.txt";
File.read(inputFilePath)
.transform((content) => {
return ">>" + content;
})
.write(outputFilePath);
File.prototype.then
というメソッドは、
this.promise.then
が返す新しいpromiseオブジェクトを this.promise
に対して代入しています。
これはどういうことなのかというと、以下のように擬似的に展開してみると分かりやすいでしょう。
const File = require("./fs-promise-chain");
File.read(inputFilePath)
.transform((content) => {
return ">>" + content;
})
.write(outputFilePath);
// => 擬似的に以下のような流れに展開できる
promise.then(() => {
return fs.readFileSync(filePath, "utf-8");
}).then((content) => {
return ">>" + content;
}).then(() => {
return fs.writeFileSync(filePath, data);
});
promise = promise.then(…)
という書き方は一見すると、上書きしているようにみえるため、
それまでのpromiseのchainが途切れてしまうと思うかもしれません。
イメージとしては promise = addPromiseChain(promise, fn);
のような感じになっていて、
既存のpromiseオブジェクトに対して新たな処理を追加したpromiseオブジェクトを作って返すため、
自分で逐次的に処理する機構を実装しなくても問題ないわけです。
fs-method-chain.jsとPromise版の違いを見ていくと、 そもそも両者には同期的、非同期的という大きな違いがあります。
fs-method-chain.js のようなメソッドチェーンでもキュー等の処理を実装すれば、 非同期的なほぼ同様のメソッドチェーンを実装できますが、複雑になるため今回は単純な同期的なメソッドチェーンにしました。
Promise版はコラム: Promiseは常に非同期?で紹介したように 常に非同期処理となるため、promiseを使ったメソッドチェーンも非同期となっています。
fs-method-chain.jsにはエラーハンドリングの処理は入っていないですが、
同期処理であるため全体を try-catch
で囲むことで行えます。
Promise版 では内部で利用するpromiseオブジェクトの
then
と catch
へのエイリアスを用意してあるため、通常のpromiseと同じように catch
によってエラーハンドリングが行えます。
const File = require("./fs-promise-chain");
File.read(inputFilePath)
.transform((content) => {
return ">>" + content;
})
.write(outputFilePath)
.catch((error) => {
console.error(error);
});
fs-method-chain.jsに非同期処理を加えたものを自力で実装する場合、 エラーハンドリングが大きな問題となるため、非同期処理にしたい時は Promiseを使うと比較的簡単に実装できるといえるかもしれません。
このメソッドチェーンと非同期処理を見てNode.jsに慣れている方は Stream が思い浮かぶと思います。
Stream を使うと、
this.lastValue
のような値を保持する必要がなくなることや大きなファイルの扱いが改善されます。
また、Promiseを使った例に比べるとより高速に処理できる可能性が高いと思います。
readableStream.pipe(transformStream).pipe(writableStream);
そのため、非同期処理には常にPromiseが最適という訳ではなく、 目的と状況にあった実装をしていくことを考えていくべきでしょう。
Note
|
Node.jsのStreamはEventをベースにしている技術 |
Node.jsのStreamについて詳しくは以下を参照して下さい。
話を戻してfs-method-chain.jsとPromise版の両者を比べると、 内部的にもかなり似ていて、同期版のものがそのまま非同期版でも使えるような気がします。
JavaScriptでは動的にメソッドを定義することもできるため、 自動的にPromise版を生成できないかということを考えると思います。 (もちろん静的に定義する方が扱いやすいですが)
そのような仕組みはES Promisesにはありませんが、
著名なサードパーティのPromise実装である bluebird などには
Promisification という機能が用意されています。
また、Node.jsのコアモジュールであるutil
モジュールには、 util.promisify というAPIが用意されています。
これを利用すると以下のように、その場でPromise版のメソッドを作成して利用できるようになります。
const fs = require("fs");
const util = require("util");
// コールバック版のAPIからPromise版を作成する
const readFile = util.promisify(fs.readFile);
readFile("myfile.js", "utf8").then((contents) => {
console.log(contents);
}).catch((e) => {
console.error(e.stack);
});
先ほどのutil.promisify
が何をやっているのか少しイメージしにくいので、
次のようなネイティブ Array
のPromise版となるメソッドを動的に定義する例を考えてみましょう。
JavaScriptにはネイティブにもDOMやString等メソッドチェーンが行える機能が多くあります。
Array
もその一つで、map
や filter
等のメソッドは配列を返すため、メソッドチェーンが利用しやすい機能です
link:src/promise-chain/array-promise-chain.js[role=include]
ネイティブのArrayと ArrayAsPromise
を使った場合の違いは
上記のコードのテストを見てみるのが分かりやすいでしょう。
link:test/array-promise-chain-test.js[role=include]
ArrayAsPromise
でもArrayのメソッドを利用できているのが分かります。
先ほどと同じように、ネイティブのArrayは同期処理で、ArrayAsPromise
は非同期処理という違いがあります。
ArrayAsPromise
の実装を見て気づくと思いますが、Array.prototype
のメソッドを全て実装しています。
しかし、array.indexOf
など Array.prototype
には配列を返さないものもあるため、全てをメソッドチェーンにするのは不自然なケースがあると思います。
ここで大事なのが、同じ値を受けるインターフェースを持っているAPIはこのような手段でPromise版のAPIを自動的に作成できるという点です。 このようなAPIの規則性を意識してみるとまた違った使い方が見つかるかもしれません。
Note
|
先ほどの util.promisify は、
Node.jsのCoreモジュールの非同期処理には また、Node.js 10からは |
このセクションでは以下のことについて学びました。
-
Promise版のメソッドチェーンの実装
-
Promiseが常に非同期の最善の手段ではない
-
Promisification
-
統一的なインターフェースの再利用
ES PromisesはCoreとなる機能しか用意されていません。 そのため、自分でPromiseを使った既存の機能のラッパー的な実装をすることがあるかもしれません。
しかし、何度もコールバックを呼ぶEventのような処理がPromiseには不向きなように、 Promiseが常に最適な非同期処理という訳ではありません。
その機能にPromiseを使うのが最適なのかを考えることはこの書籍の目的でもあるため、 何でもPromiseにするというわけではなく、その目的にPromiseが合うのかどうかを考えてみるのもいいと思います。