2022年10月18日

プロミシフィケーション

「プロミシフィケーション」は、単純な変換を表す長い単語です。コールバックを受け入れる関数を、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回だけ呼び出す関数を対象としています。それ以降の呼び出しは無視されます。

チュートリアルマップ

コメント

コメントする前にこちらをお読みください…
  • 改善するための提案がある場合は、コメントするのではなく、GitHub issueを送信するか、プルリクエストを送信してください。
  • 記事の中で何か理解できないことがある場合は、詳しく説明してください。
  • 数語のコードを挿入するには、<code>タグを使用し、複数行の場合は<pre>タグで囲み、10行以上の場合にはサンドボックス(plnkrjsbincodepen…)を使用してください。