「プロミシフィケーション」は、単純な変換を表す長い単語です。コールバックを受け入れる関数を、Promiseを返す関数に変換することです。
多くの関数やライブラリがコールバックベースであるため、このような変換は実生活でよく必要になります。しかし、Promiseの方が便利なので、それらをプロミシファイするのは理にかなっています。
理解を深めるために、例を見てみましょう。
たとえば、入門: コールバックの章で学んだloadScript(src, callback)
があるとします。
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));
document.head.append(script);
}
// usage:
// loadScript('path/script.js', (err, script) => {...})
この関数は、与えられたsrc
でスクリプトをロードし、エラーが発生した場合はcallback(err)
を呼び出し、ロードが成功した場合はcallback(null, script)
を呼び出します。これはコールバックを使用する一般的な規約であり、以前にも確認しました。
これをプロミシファイしてみましょう。
新しい関数loadScriptPromise(src)
を作成します。これは同じことを(スクリプトをロードする)しますが、コールバックを使用する代わりにPromiseを返します。
言い換えれば、src
だけを渡して(callback
なし)、ロードが成功した場合はscript
で解決され、それ以外の場合はエラーで拒否されるPromiseを返します。
以下がそうです。
let loadScriptPromise = function(src) {
return new Promise((resolve, reject) => {
loadScript(src, (err, script) => {
if (err) reject(err);
else resolve(script);
});
});
};
// usage:
// loadScriptPromise('path/script.js').then(...)
ご覧のとおり、新しい関数は元のloadScript
関数をラップしたものです。Promiseのresolve/reject
に変換する独自のコールバックを提供して呼び出します。
これで、loadScriptPromise
はPromiseベースのコードによく適合します。コールバックよりもPromiseを好む場合(そしてすぐにそのための多くの理由がわかります)、代わりにそれを使用します。
実際には、複数の関数をプロミシファイする必要がある場合があるため、ヘルパーを使用するのが理にかなっています。
それをpromisify(f)
と呼びます。これは、プロミシファイする関数f
を受け取り、ラッパー関数を返します。
function promisify(f) {
return function (...args) { // return a wrapper-function (*)
return new Promise((resolve, reject) => {
function callback(err, result) { // our custom callback for f (**)
if (err) {
reject(err);
} else {
resolve(result);
}
}
args.push(callback); // append our custom callback to the end of f arguments
f.call(this, ...args); // call the original function
});
};
}
// usage:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);
コードは少し複雑に見えるかもしれませんが、本質的には、loadScript
関数をプロミシファイする際に上で書いたものと同じです。
promisify(f)
の呼び出しは、f
のラッパー(*)
を返します。そのラッパーはPromiseを返し、元のf
への呼び出しを転送し、カスタムコールバック(**)
で結果を追跡します。
ここで、promisify
は、元の関数が正確に2つの引数(err, result)
を持つコールバックを期待すると想定しています。これは最も頻繁に遭遇するものです。次に、カスタムコールバックはまさに正しい形式であり、promisify
はこの場合に最適に機能します。
しかし、元のf
がより多くの引数callback(err, res1, res2, ...)
を持つコールバックを期待する場合はどうすればよいでしょうか?
ヘルパーを改善できます。promisify
のより高度なバージョンを作成しましょう。
promisify(f)
として呼び出されると、上記のバージョンと同様に動作する必要があります。promisify(f, true)
として呼び出されると、コールバック結果の配列で解決されるPromiseを返す必要があります。これはまさに、多くの引数を持つコールバックの場合です。
// promisify(f, true) to get array of results
function promisify(f, manyArgs = false) {
return function (...args) {
return new Promise((resolve, reject) => {
function callback(err, ...results) { // our custom callback for f
if (err) {
reject(err);
} else {
// resolve with all callback results if manyArgs is specified
resolve(manyArgs ? results : results[0]);
}
}
args.push(callback);
f.call(this, ...args);
});
};
}
// usage:
f = promisify(f, true);
f(...).then(arrayOfResults => ..., err => ...);
ご覧のとおり、基本的には上記と同じですが、manyArgs
が真であるかどうかに応じて、resolve
は1つまたはすべての引数で呼び出されます。
callback(result)
のように、err
がまったくないような、よりエキゾチックなコールバック形式の場合は、ヘルパーを使用せずに、そのような関数を手動でプロミシファイできます。
es6-promisifyなど、もう少し柔軟なプロミシフィケーション関数を持つモジュールもあります。Node.jsには、それのための組み込みのutil.promisify
関数があります。
プロミシフィケーションは、特にasync/await
(後のAsync/awaitの章で取り上げます)を使用する場合は優れたアプローチですが、コールバックの完全な代替にはなりません。
Promiseは1つの結果しか持つことができませんが、コールバックは技術的に何度も呼び出すことができることを覚えておいてください。
したがって、プロミシフィケーションは、コールバックを1回だけ呼び出す関数を対象としています。それ以降の呼び出しは無視されます。
コメント
<code>
タグを使用し、複数行の場合は<pre>
タグで囲み、10行以上の場合にはサンドボックス(plnkr、jsbin、codepen…)を使用してください。