ECMAScript 2015では非同期処理を行うためにPromiseオブジェクトが導入された。今回はこの基本的な使い方を見ていこう。
powered by Insider.NET
前回はBabelのコマンドラインインターフェースを使用して、ECMAScript 2015(以下、ES2015)のモジュールについて調べた。今回はES2015で採用されたPromiseオブジェクトによる非同期処理について見ていこう。
ES2015ではより簡潔な形で非同期処理を行えるようにPromiseオブジェクトが導入されている。以下では、これまでの非同期処理を簡単に見た後、Promiseオブジェクトの使い方の基本を見る。
JavaScript 5.x(以下、JS5)はシングルスレッドな言語であり、その実行は基本的に線形に行われる。そのため、非同期処理を行うにはイベント機構やsetTimeout関数、コールバック関数を組み合わせるなどして行われていた。
setTimeout(function() {
console.log('hello from 1000ms after');
}, 1000);
console.log('hello from outer setTime func');
こうした手法を使えば、JS5でも何らかの処理を非同期に実行できる。ただし、「この処理が完了したら、このコールバック関数を呼び出してもらって、それが完了したら、今度はあのコールバック関数を呼び出してもらって……」というコードを書くことになるので、一つの関数の中でいくつも無名関数がネストするなど、コードはあまり読みやすいものにはならない。
var fs = require('fs');
var http = require('http');
function readFileA(filename, func) {
fs.readFile(filename, 'utf8', function(err, data) {
…… 読み出しが完了したらコールバック「func」を呼び出す ……
});
}
function getContentsA(url, func) {
var contents = '';
var req = http.request({ host: url, method: 'GET' }, function(res) {
…… 読み出しが完了したらコールバック「func」を呼び出す ……
});
…… 省略 ……
}
readFileA('urls.txt', function(data) {
var urls = data.trim().split(/(?:\r\n|[\r\n])/);
urls.forEach(function(url) {
getContentsA(url, function(contents) {
console.log(contents);
});
});
});
これに対して、ES2015で導入されたPromiseオブジェクトを使うと、よりすっきりとした形で非同期処理を記述できるようになる。
Promiseオブジェクトを使用した場合の典型的な非同期処理の記述は次のようになる。
var promise = new Promise(function(resolve, reject) { ... });
promise.then(function(value) { ... }, function(reason) { ... });
プロミス(Promiseオブジェクト)は「何らかの非同期処理の最終結果」を表現する。このオブジェクトの作成時には、そのプロミスで実行する処理を関数として渡す。この関数が非同期処理を行う実体となるが、通常、この関数は二つのコールバック関数をパラメーターに取る。
一つは非同期処理の完了時(成功時)に呼び出す関数、もう一つは非同期処理の失敗時に呼び出す関数だ。前者を「resolve」、後者を「reject」と表記することがよくある。「resolve」と「reject」の二つの関数の実体はプロミスの「then」メソッドに引数として渡す。上で典型としたコードではこれを行っている。
それぞれのコールバック関数のパラメーター「value」と「reason」はそれぞれ、非同期処理の完了によって得られた「値」と非同期処理が完了しなかった「理由」を示すオブジェクトだ(これらはプロミスとして実行する非同期処理内でresolve/rejectコールバック関数を呼び出す際に引数としてセットする)。
といっても分かりにくいので、簡単なコード例を示しておこう。以下はカレントディレクトリにある「package.json」ファイルを読み出して、それをJSON形式にパースしたものをコンソールに出力するコードだ。
var promise = new Promise(function(resolve, reject) {
fs.readFile('package.json', function(err, data) {
if (!err) {
resolve(data);
} else {
reject(err);
}
});
});
promise.then(function(value) { // resolveコールバック関数
var pkginfo = JSON.parse(value);
console.log(pkginfo);
}, function(reason) { // rejectコールバック関数
console.log(reason.message);
});
ここで作成したPromiseオブジェクトは、Node.jsのファイルシステムアクセスAPIを使用して、ローカルなファイルを読み出している。このときに呼び出しているreadFileメソッドは非同期に実行される。そして、読み出しが完了すると、コールバック関数「function(err, data)」が呼び出される。その内部では、エラーがなければコールバック関数「resolve」を、エラーが発生時にはコールバック関数「reject」を呼び出している。それぞれのコールバック関数では渡された引数の値を使用できる。
このプロミスのthenメソッドには二つのコールバック関数を渡して、成功時には得られた値をJSON.parseメソッドでパースしてコンソールに出力し、失敗時にはその原因を示すメッセージをコンソールに出力している。
このように、非同期に行う処理を表すPromiseオブジェクトを作成して、それが成功/失敗したら(すなわち「then」)行う処理をさらに記述していくのが、Promiseオブジェクトの使い方の基本型だ。次ページでは例をもう少し見てみよう。
本稿のサンプルコードはBabelのコマンドラインインターフェース(babel-cli)をインストールした環境で実行できる。実行にはbabel-cliパッケージだけではなく、BabelでES2015コードをJS5コードにトランスパイルするためのプリセットである「babel-preset-es2015」モジュールも必要になる(「npm install babel-preset-es2015」コマンドを実行)。
また、Promiseモジュールのインストールが必要な場合には適宜「npm install promise」コマンドを実行しておく(この場合はスクリプトの先頭で「var Promise = require('promise');」などを行う必要があるかもしれない)。
この場合は以下のようなコマンドラインを実行する(スクリプトは「index.es6」ファイルに保存したものとする)。
> babel-node --presets es2015 index.es6
{ name: 'promisetest',
version: '1.0.0',
description: '',
main: 'index.js',
scripts: { start: 'babel-node --presets es2015 index.es6' },
author: '',
license: 'ISC' }
また、Node.jsはES2015の機能の一部をサポートしているので、Babelを使用せず、以下のように「--harmony」オプションを指定してスクリプトファイルを実行してもよい。
> node --harmony index.es6
{ name: 'promisetest',
…… 省略 ……
Copyright© Digital Advantage Corp. All Rights Reserved.