Promiseオブジェクトはthenメソッドとcatchメソッドのチェーンにより、処理を連続させることで、コールバック関数を連鎖させることなく非同期処理を簡潔に記述できるのが大きなメリットである。
このチェーンの中に「Promiseオブジェクト以外のオブジェクトを組み込めない」かというとそうでもない。そうした場面で使えるのが「thenable」オブジェクトだ。これは「thenメソッドを呼び出し可能」つまりPromiseオブジェクト的に扱えるオブジェクトのことだ。
では、thenメソッドとはどんなものだっただろう。それはもちろん、resolve関数とreject関数を引数に受け取り、それを使って、Promiseオブジェクトが完了したときに対応する処理を行うメソッドだ。そういうわけで、これと同様な処理を行う関数あるいはオブジェクトであれば、チェーンの中でこれを使用できる。
以下に簡単な例を示す。
var thenableobj = {
then: function(resolve, reject) {
setTimeout(() => resolve("always resolved"), 1000);
}
};
console.log("begin");
var p = Promise.resolve(thenableobj);
p.then(
value => console.log(value),
reason => console.log(reason)
)
console.log("end");
最初にthenableobjオブジェクトを作成している。このオブジェクトは、thenメソッドを持っている。これはresolve関数とreject関数を受け取る。ここでは、常にresolve関数を呼び出すだけだが、これによりこのオブジェクトは「Promiseオブジェクトの作法に従ったthenメソッド呼び出し」が可能になっている。
次に、これを先ほど紹介したPromise.resolveメソッドに渡すことで、Promiseオブジェクト化している。これにより、その下で行っているようにthenメソッドを呼び出せる
今度はthenableな関数の例を見てみよう。この関数はthenメソッドを持つオブジェクトを返す。そして、thenメソッドの中では引数conditionの値で、resolve関数/reject関数のいずれかを呼び出すようになっている。
function thenablefunc(condition) {
return {
then: function(resolve, reject) {
if (condition) {
resolve("resolved");
} else {
reject("rejected");
}
}
};
}
console.log("begin");
var p = Promise.resolve(thenablefunc(false));
p.then(
value => console.log(value),
reason => console.log(reason)
);
console.log("end");
これまでの二つの例を見て「どっちもthenメソッドがあるんだから、Promiseオブジェクトにしなくたっていいんじゃない?」と思った人もいるかもしれない(というか、筆者がそうだ)。つまり、以下のようなコードを書いても問題ないのではないか、という話だ。
function thenablefunc(condition) {
return {
then: function(resolve, reject) {
…… 省略 ……
}
};
}
console.log("begin");
var p = Promise.resolve(thenablefunc(false));
thenablefunc(false).then(
value => console.log(value),
reason => console.log(reason)
);
console.log("end");
実際に試してみると分かるが、この場合、thenablefunc関数の呼び出しは同期的に行われ、その戻り値に対するthenメソッド呼び出しも同期的に行われる。そのため、上の二つのコードの実行結果は異なるものになる。
※Promise.resolve(thenablefunc(false))に対してthenメソッドを呼び出した場合
>node --harmony promisetest.js
begin
end
rejected
※thenablefunc(false)の戻り値に対してthenメソッドを呼び出した場合
>node --harmony promisetest.js
begin
rejected
end
もちろん、thenメソッドを持ち、Promiseオブジェクトライクな動作をすれば、それはthenableオブジェクトなので、後者の使い方が悪いというわけではないと思われる(ただし、Babelでそうしたコードを実行してみたところ、チェーンの中で「thenメソッドが見つからないよ?」というエラーが発生したことは追記しておこう)。が、実行が同期的か非同期的かの違いはあるので、注意はしておこう。
だが、上で試したthenableな関数呼び出しに対してthenメソッドを呼び出す場合(thenablefunc(false).then(...))、これはPromiseオブジェクトを返すわけではないので、これ以上のチェーンはできない。ということは、やはりPromise.resolveメソッドによるPromiseオブジェクト化は必須と考えるべきだ(上記のエラーもこれが原因だろう)。
もちろん、thenableオブジェクト内部でコールバックによる非同期処理を行っていれば、それは非同期に実行される。以下は前回作成したgetWebPageAsync関数をthenableオブジェクト化したものだ。thenメソッドを持つオブジェクトを返すようにしただけだ。
var http = require('http');
function getWebPageThenable(url) {
return {
then: 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();
}
};
}
console.log("begin");
var p = Promise.resolve(getWebPageThenable("www.microsoft.com"));
p.then(
value => console.log(value),
reason => console.log(reason)
);
console.log("end");
興味のある方は、この実行結果についても検討してみてほしい。
今回はPromiseオブジェクトについて深掘りをした。次回はECMAScriptの次バージョンで採用されるといわれているasync/awaitによる非同期処理を見ていこう。
Copyright© Digital Advantage Corp. All Rights Reserved.