はらへり日記

腹に弾丸

これからのJSの非同期処理関数は全てPromiseを返させるべき

はじめに

JSで非同期関数を書く時、個人的に意識してる話です。

別にTipsとかじゃないです。

要するにポエムです。

あしからず(´・ω・`)

非同期関数を使いこなす

JSを書いたことがある人なら知ってるであろう非同期処理ですが、僕は非同期処理はなるべくPromiseオブジェクトを返す関数に切り分けていくべきだと思っています。

極端な例として「APIその1を叩いて、その結果からAPIその2を叩いて、その結果からAPIその3を叩いて、それを画面に描画するぞい!」というコードを書いてみます。

悪い例:関数で処理をラップしない

APIが綺麗に整っていれば最高ですが、現実は得てしてあまくありません。

3種類のAPIを叩くわけですがそれらのレスポンススキーマは全て違うとしましょう。

const firstApiUrl = 'http://api1.com';
const secondApiUrl = 'http://api2.com';
const thirdApiUrl = 'http://api3.com';

// 自前で定義したajax()関数をがあるとします
ajax(firstApiUrl, { data: 'data' }, (err, res) => {
  if (err) {
    alert('APIその1のエラーだよ');
    return;
  }
  
  if (res.status) {
    // APIその1から返ったデータを使う
    ajax(secondApiUrl, { data: res.body.data }, (err, res) => {
      if (err) {
        alert('APIその2のエラーだよ');
        return;
      }
      
      if (res.result) {
        // APIその2から返ったデータを使う
        ajax(thirdApiUrl, { data: res.count }, (err, res) => {
          if (err) {
            alert('APIその3のエラーだよ!');
          } 
          
          $('body').append(res.html);
        });
      }
    });
  }
});

はい。いわゆるネスト地獄ですね。

このコードの悪いところをざっとあげてみると

  • ネストが深すぎて可読性が低い
  • レスポンススキーマAPIごとに違うので同じような処理なのにデータの取り出し方が違ってなんか気持ち悪い(res.body.data, res.result, res.count.....)
  • 『上司「APIその2のレスポンススキーマ変わるから対応してよ」』って時にその処理部分を探すのが大変(grepかけますか???)

悪い例:コールバック関数を引数に取る関数を書く

先ほどあがっていた悪い点のうち、2点目を解決してみましょう。

処理を関数でわけてみます。

/**
 * @description APIその1と通信する
 * @param {mixed} data
 * @param {Function} callback
 */
const ajaxFirstApi = (data, callback) => {
  let baseUrl = 'http://api1.com';
  ajax(baseUrl, { data: data }, (err, res) => {
    if (err) {
      alert('APIその1の通信エラーです');
      callback(err, null);
    }
    
    callback(null, res.body.data);
  })
};

/**
 * @description APIその2と通信する
 * @param {mixed} data
 * @param {Function} callback
 */
const ajaxSecondApi = (data, callback) => {
  let baseUrl = 'http://api2.com';
  ajax(baseUrl, { data: data }, (err, res) => {
    if (err) {
      alert('APIその2の通信エラーです');
      callback(err, null);
    }
    
    callback(null, res.result);
  })
};

/**
 * @description APIその3と通信する
 * @param {mixed} data
 * @param {Function} callback
 */
const ajaxThirdApi = (data, callback) => {
  let baseUrl = 'http://api3.com';
  ajax(baseUrl, { data: data }, (err, res) => {
    if (err) {
      alert('APIその3の通信エラーです');
      callback(err, null);
    }
    
    callback(null, res.count);
  })
};

ajaxFirstApi('data', (err, result) => {
  if (result) {
    ajaxSecondApi(result, (err, result) => {
      if (result) {
        ajaxThirdApi(result, (err, result) => {
          if (result) {
            $('body').append(result);
          }
        }
      }
    });
  }
});

コード量が増えたものの、先程よりは多少よくなったのではないでしょうか。

最初の例と比べると以下の点が改善されたかと思います。

  • それぞれのAPIへのアクセスを関数化したので使い回しがきく/修正が容易
  • レスポンススキーマについては関数の中で完結してるので実際に関数を使う時に意識しなくてよい

しかし、以下の問題については未だ未解決です。

  • ネストが深くて可読性が低い

また、このアプローチだと以下のような関数が混ざった時に混乱します。

/**
 * @description APIその4と通信
 * @param {mixed} data
 * @param {Function} calllback
 */
const ajaxForthApi = (data, callback) => {
  ajax('http://api4.com', { data: data }, (err, res) => {
    callback(res, err); // ここに注目!!!
  });
};

先ほど定義したajaxFirstApi(), ajaxSecondApi(), ajaxThirdApi()ではcallbackに渡される変数の順番が最初にerr, ついでresとなっていました。

しかしここで定義されている関数ではその順番が逆になっています。

これには2つの問題があり、

  • 関数の使用方法が統一されない コーディングルールで決めることは可能だがLint等の自動ツールでのチェックはほぼ不可能であり、実装者を完全に信用しなければいけない

  • コールバック関数に渡される引数の順番を知らないと関数が使えない ajaxFirstApi('data', (err, res) => {});というように使いたい時、第二引数の関数に最終的に何が渡されるかを実装者は知る必要があります。

丁寧にJSDoc等でコメントが書かれていればそれで済みますが、世のコードの9割はそんなに親切ではなく結局実装を追う羽目になることが多いと思います。(せっかく関数化したのに…)

良い例:全ての関数にPromiseを返させる

待たせたな!ここでPromiseを使ってみましょう!

/**
 * @description APIその1と通信する
 * @param {mixed} data
 * @return {Promise}
 */
const ajaxFirstApi = (data) => {
  let baseUrl = 'http://api1.com';
  return new Promise((resolve, reject) => {
    ajax(baseUrl, { data: data }, (err, res) => {
      if (err) {
        alert('APIその1の通信エラーです');
        reject(err);
      }
    
      resolve(res.body.data);
    });
  })
};

/**
 * @description APIその2と通信する
 * @param {mixed} data
 * @return {Promise}
 */
const ajaxSecondApi = (data) => {
  let baseUrl = 'http://api2.com';
  return new Promise((resolve, reject) => {
    ajax(baseUrl, { data: data }, (err, res) => {
      if (err) {
        alert('APIその2の通信エラーです');
        reject(err);
      }
    
      resolve(res.result);
    });
  })
};

/**
 * @description APIその3と通信する
 * @param {mixed} data
 * @return {Promise}
 */
const ajaxThirdApi = (data) => {
  let baseUrl = 'http://api3.com';
  return new Promise((resolve, reject) => {
    ajax(baseUrl, { data: data }, (err, res) => {
      if (err) {
        alert('APIその3の通信エラーです');
        reject(err);
      }
    
      resolve(res.count);
    });
  })
};

ajaxFirstApi('data') 
  .then((result) => {
    return ajaxSecondApi(result);
  })
  .then((result) => {
    return ajaxThirdApi(result);
  })
  .then((result) => {
    $('body').append(result);
  })
  .catch((err) => {
    alert('通信エラーです');
  });

// 省略してこう書くことも可能

ajaxFirstApi('data') 
  .then(ajaxSecondApi)
  .then(ajaxThirdApi)
  .then((result) => {
    $('body').append(result);
  })
  .catch((err) => {
    alert('通信エラーです');
  });

さぁ!前回の例での問題が解決されました。

前回の問題として以下のものがありました。

  • ネストが深い
  • コールバック関数に渡される引数の順番を知る必要がある

コード量は増えましたが、Promiseを返す関数はthen()catch()を使ってつなげることができます。

最新のJSではAPIでもこのthenableの考え方が浸透しており、これを使って非同期処理の順番をネストの深さを変えずにつなげることが可能になります。

コールバック関数に渡される引数の順番についても知る必要はなくなりました。

Promiseを返す関数ではresolve()reject()の2種類に縛られます。

前者は成功時の返り値、後者はエラー時の返り値となります。

そしてそれらをthen()catch()で捉えて、行いたい処理を書きます。

ね、簡単でしょう?

何が嬉しいのか

嬉しさポイントは見た目が綺麗になることもありますが、個人的にはコールバック関数の形式が強制的にresolve(), reject()で縛られるのがいいかなと思います。

今まではコールバック関数に何が返ってくるのかドキュメントを読む必要がありましたが、Promise化されていれば成功か失敗かで考えるだけで済みます。

あるAPIを叩く関数をPromise化しておけば内部実装がどうなっていようと使う側はthen()で結果を受け取り、catch()でエラーハンドリングを行えばよいのです。

ある程度冗長には見えますがこれからはPromiseが常識になっていきますし非同期関数は全部Promiseでいいんじゃないかなと思ってます。

実際に使いたいけど動かないんでしょう?

いいえ!動きます。

IE8まではpolyfillライブラリであるes6-promiseを使用することで問題なく使うことができます。

もっと知りたい

Promiseの本がおすすめです。

azu.github.io

まとめ

みんなPromiseで幸せになろう!!!!!!!!!必ずなろう!!!!!!!