C#開発者のための最新JavaScript事情(Promise編):特集:C#×JavaScript(2/2 ページ)
ECMAScript 2015では非同期処理を行うためにPromiseオブジェクトが導入された。今回はこの基本的な使い方を見ていこう。
ファイルの読み込み
以下に示すのは、冒頭に示したファイル読み込み処理を関数にまとめたものだ。コードは見ての通りのものなので説明は割愛する。ファイル読み込みを行うPromiseオブジェクトを関数の戻り値としているので、関数呼び出しにthenメソッドをそのまま追記していける。
var fs = require('fs');
function readFileAsync(filename) {
return new Promise(function(resolve, reject) {
fs.readFile(filename, 'utf8', function(err, data) {
if (!err) {
resolve(data);
} else {
reject(err);
}
});
});
}
// readFileAsync関数の使用例
readFileAsync('package.json').then(function(data) {
console.log(JSON.parse(data));
}).catch(function(reason) {
console.log(err);
});
readFileAsync('urls.txt').then(function(data) {
var urls = data.trim().split(/(?:\r\n|[\r\n])/);
console.log(urls);
});
ここで注目してほしいのは二つ。一つは「catch」メソッドだ。もう一つは二つ目の使用例ではrejectコールバック関数を呼び出していないことだ。
先ほど、thenメソッドには二つの関数を引数として渡すと述べたが、rejectコールバック関数に関してはcatchメソッドに渡してもよい。実際にはresolveコールバック関数もrejectコールバック関数もオプションとなっている(ただし、どちらかを渡さないと何も起きない)。catchメソッドは実質的には「then(undefined, function() {...}」と同様に考えてよい。
ただし、thenメソッドにrejectコールバック関数を渡した場合には、そのプロミスでエラーが発生したときにはreject関数が呼び出されるが、それ以降にエラーが発生した場合にはそのエラーを処理することはできない。then/catchメソッドのチェーンについては後で紹介するが、エラーをまとめてトラップするのであれば、専用のハンドラーを用意して次のように記述できる。
var promise = new Promise(...);
function err_handler() { ... }
promise
.then(...)
.then(...)
.catch(err_handler);
Webページの読み出し
もう一つ例を見てみよう。これはNode.jsのhttpモジュールを使用して、Webページの内容を取得するgetWebPageAsync関数だ。
var http = require('http');
function getWebPageAsync(url) {
return new Promise(function(resolve, reject) {
var contents = '';
var req = http.request({ host: url, method: 'GET' }, function(res) {
res.on('data', function(chunk) {
contents += chunk;
});
res.on('end', function() {
resolve(contents);
});
});
req.on('error', function(reason) { reject(reason.message); });
req.end();
});
}
// 使用例
getWebPageAsync('www.buildinsider.net')
.then(contents => console.log(contents));
引数に渡されたホスト名を使用して、Webページの内容を取得している。取得が終わったら、resolveコールバック関数を呼び出し、何らかのエラーが発生したらrejectコールバック関数を呼び出すようになっている。
使用例では、アロー関数を使用して、記述を簡潔にした。実際には、以下のようにもっと簡潔に記述もできる。
getWebPageAsync('www.buildinsider.net')
.then(console.log);
正直、どうでもいいような内容の二つの関数を紹介したのは、これらを使って二つのことを試してみたいからだ。一つは、複数のプロミスの非同期実行。もう一つは、プロミスチェーンだ。
Promise.allメソッド
同じような処理をプロミスを使って行う場合、それらをまとめて処理することもできる。これにはPromise.allメソッドを使用する。実際には、配列のmapメソッドを使用して、配列の要素にプロミスを生成する関数を適用し、それらをPromise.allメソッドで処理するのが一般的な手順になる。以下に例を示す。
var urls = [
'www.google.com',
'www.microsoft.com'
];
var promiselist = urls.map(getWebPageAsync)
Promise.all(promiselist)
.then(contents => console.log(contents));
ここではURLとなる文字列を配列に格納して、mapメソッドでその要素にgetWebPageAsync関数を適用している。getWebPageAsync関数は「プロミスを返す」ので、変数promiselistには「プロミスの配列」が格納される。
Promise.allメソッドはこれらの配列の状態を表すプロミスを返す。そして、全てのプロミスが終了するとthenメソッドが呼び出される。このときに渡される値(上の例ではcontents引数)は、それぞれの処理結果を要素とする配列となる(contents[0]、contents[1]などとしてアクセス可能。上の例ではまとめてコンソールに出力している)。
プロミスチェーン
ここまでが準備だ。本稿の最後に、プロミスチェーン(then/catchメソッドの連結実行)を見てみよう。ここではurls.txtファイルからURLの一覧を取得して、それぞれのページの内容を読み込んで、コンソールに出力する。
var fs = require('fs');
var http = require('http');
function readFileAsync(filename) {
return new Promise(function(resolve, reject) {
…… 省略 ……
});
}
function getWebPageAsync(url) {
return new Promise(function(resolve, reject) {
…… 省略 ……
});
}
readFileAsync('urls.txt')
.then(data => {
var urls = data.trim().split(/(?:\r\n|[\r\n])/);
return Promise.all(urls.map(getWebPageAsync)); })
.then(contents => console.log(contents))
.catch(reason => console.log(reason));
then/catchメソッドはチェーンできる。これは、これらのメソッドが「プロミスを返送し、Promiseはthen/catchメソッドを持つ」からだ(今回は紹介しなかったが、Promiseオブジェクトには「thenable」という概念もある。これについては次回紹介するが、簡単には「Promiseオブジェクトでなくともthenメソッド呼び出しと同様な挙動を示すオブジェクトはプロミスチェーンの中から呼び出せる」という概念だ)。
ここでは、readFileAsync関数が返すプロミスを処理するresolveコールバック関数(アロー関数を使って記述)内で、読み込んだ文字列(改行で区切られたURL一覧)から前後の空白文字(改行文字)を削除したものを、その途中に含まれている改行文字をデリミターとして分解して、配列に格納している。URL一覧が配列になったので、後は上で見たように、これをプロミスの配列にして(urls.mapメソッド呼び出し)、それをPromise.allメソッドに食わせたものを戻り値としている。ここまでが、最初のthenメソッドで行っていることだ。
その後は、全ての処理が完了した時点で、そのページの内容をコンソールに出力し、エラー発生時にはそのことをコンソールに出力するようにしているだけだ。
このように、Promiseオブジェクトを使用すると、then/catchメソッドをさせながら極めて簡潔な形で非同期処理を記述できるようになる。アロー関数を使用すると「function() { ... }」という記述もなくせるので、さらに簡潔になる。
ページと時間が尽きてきたので(主に時間)、今回はここまでとしよう。次回はPromiseオブジェクトについてもう少し掘り下げてみたいと思う。
Copyright© Digital Advantage Corp. All Rights Reserved.