コンテンツにスキップ

非同期処理ロジック@JavaScript

はじめに

本サイトにつきまして、以下をご認識のほど宜しくお願いいたします。


01. 非同期処理

非同期処理化

処理の実行を部分的に遅らせると、後続する処理が先に実行される。

これは非同期処理である。

*実装例*

非同期化する処理をsetTimeout関数に渡し、処理を遅らせる。

function asyncMethod() {
  // 1秒だけ実行を遅らせる
  setTimeout(function () {
    console.log("foo");
  }, 1000);

  console.log("bar");
}

asyncMethod();

// 先にbarが実行される
// bar
// foo


02. JavaScript Promise

JavaScript Promiseとは

JavaScriptで、非同期処理の成否を管理し、後続する処理を定義できるオブジェクトのこと。

Promiseオブジェクトのコンストラクタに、非同期処理を持つ関数を渡すことにより、Promiseオブジェクトはこの関数内の非同期処理の成否を管理する。

実装量を減らして同じことを実装する場合、async宣言を使用する。

Promiseオブジェクトの実装の仕様は取り決められており、以下のリンクを参考にせよ。

const asyncFunc = () => {
  return new Promise(
    // 非同期処理を持つ関数を渡す
    (resolve, reject) => {
      // 関数内の非同期処理の成否が管理される
    },
  );
};

Promiseオブジェクトの種類

ネイティブなJavaScriptのPromiseオブジェクト、JQueryのPromiseオブジェクトがある。

ネイティブの方が、Promiseオブジェクトの仕様により則った機能を持つ。

リリース日 提供 種類 説明 補足
2012 JQueryパッケージのDeferredモジュール Promiseオブジェクト バージョン1.5でPromiseオブジェクトが導入された。
https://api.jquery.com/category/version/1.5/
https://api.jquery.com/category/deferred-object/
2015 ビルトインオブジェクト Promiseオブジェクト JQueryのPromiseオブジェクトを参考にして、ES2015から新しく使用できるようになった。 https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise
2017 ビルトインオブジェクト async/await宣言 ES2017から新しく使用できるようになった。ビルトインオブジェクトのPromiseオブジェクトをより使用しやすくしたもの。 https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/async_function


resolveメソッド、rejectメソッド

▼ コンストラクタを使用する場合

Promiseオブジェクトのコンストラクタ内では、暗黙的にtry-catchが実行されている。

そのため、結果のステータスが成功であればresolveメソッドの結果を返却し、反対に失敗であればrejectメソッドを返却する。

両方を実装すると良しなに実行してくれる。

resolveメソッドとresolveメソッドのコール時にreturnを使用しないと、後続する処理も実行される。

1つ目の書き方として、Promiseインスタンスのコールバック関数に渡す方法がある。

const asyncFunc = () => {
  return new Promise((resolve, reject) => {
    // ステータスが成功の場合に選択される。
    resolve("SUCCESS"); // Promise { "SUCCESS" }

    // ステータスが失敗の場合に選択される。
    reject("FAILED"); // Promise { "FAILED" }

    console.log("test");
  });
};

console.log(asyncFunc());
// 後続する処理も実行され、resolveメソッドの結果が返却される。
// test
// Promise { 'SUCCESS' }

一方で、resolveメソッドとresolveメソッドのコール時にreturnを使用すると、後続する処理は実行されない。

const asyncFunc = () => {
  return new Promise((resolve, reject) => {
    return resolve("SUCCESS");

    reject("FAILED");

    console.log("test");
  });
};

console.log(asyncFunc());
// 後続する処理も実行されない。
// Promise { 'SUCCESS' }

▼ コンストラクタを使用しない場合

別の書き方として、Promiseオブジェクトから直接的にresolveメソッドやrejectメソッドをコールしても良い。

この場合、必ずreturnで返却する必要がある。

returnを使用しないと、何も返却されない。

const asyncFunc = () => {
  // ステータスが成功の場合に選択される。
  return Promise.resolve("SUCCESS"); // Promise { "SUCCESS" }
};

const asyncFunc = () => {
  // ステータスが失敗の場合に選択される。
  return Promise.reject("FAILED"); // Promise { "FAILED" }
};

console.log(asyncFunc()); // Promise { 'SUCCESS' }
const asyncFunc = () => {
  return Promise.resolve("SUCCESS");
};

asyncFunc()
  // 失敗時に返却されたrejectをハンドリング
  .catch((reject) => {
    // rejectメソッドを実行
    reject;
  })
  .then((resolve) => {
    // resolveメソッドを実行
    resolve;
  });

console.log(asyncFunc()); // Promise { 'SUCCESS' }

非同期処理内で両方をコールするとエラーになってしまう。

const asyncFunc = () => {
  Promise.resolve("SUCCESS");
  Promise.reject("FAILED");
};

console.log(asyncFunc()); // エラーになってしまう
UnhandledPromiseRejectionWarning: FAILED
(Use `node --trace-warnings ...` to show where the warning was created)
UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
[DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

補足として、NodeのHTTPパッケージの関数は、Promiseインスタンスのコールバック関数として使用しないと、正しく挙動しない。


thenメソッド

thenメソッドとは

Promiseオブジェクトのresolve関数の結果を引数に受け取り、コールバック関数を実行する。

▼ コンストラクタを使用する場合

*実装例*

const resolveFunc = new Promise((resolve, reject) => {
  return resolve("resolve!!");
});

resolveFunc.then((value) => {
  // resolveFuncがPromiseを返し、resolve!!がresolveされるため
  // thenメソッドが実行されコンソールにresolve!!が表示される
  console.log(value); // resolve!!
});
const resolveFunc = () => {
  // resolveFuncはasync functionではないため、Promiseを返さない
  return "resolve!!";
};

resolveFunc.then((value) => {
  // resolveFuncはPromiseを返さないため、エラーが発生して動かない
  // Uncaught TypeError: resolveError(...).then is not a function
  console.log(value);
});


catchメソッド

catchメソッドとは

Promiseオブジェクトのreject関数の結果を引数に受け取り、コールバック関数を実行する。

▼ コンストラクタを使用する場合

const rejectFunc = new Promise((resolve, reject) => {
  reject(new Error("reject!!"));
});

rejectFunc.catch((err) => {
  // rejectFuncがPromiseを返し、reject!!がrejectされるため
  // catchメソッドが実行されコンソールにreject!!が表示される
  console.error(err); // reject!!
});


finallyメソッド

finallyメソッドとは

記入中...


setTimeout

指定した秒数だけ処理を待機する。

// 5秒待機する。
await new Promise((resolve) => {
  setTimeout(resolve, 5000);
});


03. async/await

async宣言

▼ async宣言とは

async宣言された関数内の非同期処理は、Promiseオブジェクトに渡すための関数内に暗黙的に定義される。

Promiseオブジェクトを明示的に使用する場合、Promiseオブジェクトのコンストラクタに非同期処理を持つ関数を渡す必要があるため、Promiseオブジェクトの使用が楽になる。

Promiseや、これのコントラクタに渡す関数を実装する必要が無いため、可読性が高まる。

また、仮にPromiseオブジェクトをコールし、PromiseオブジェクトがPromiseオブジェクトに渡されてしまっても、結果的に入れ子にならないようによしなに処理してくれる。

*実装例*

以下の全ては、同じ処理を定義している。

const asyncFunc = async () => {
  // Promiseオブジェクトに渡すための関数内に暗黙的に定義される。
  return "SUCCESS";
};

// 単にreturnとしてもPromiseオブジェクトが返却される。
console.log(asyncFunc()); // Promise { "SUCCESS" }
const asyncFunc = async () => {
  return new Promise((resolve, reject) => {
    return resolve("SUCCESS"); // Promise { "SUCCESS" }
  });
};

// Promiseオブジェクトを返却するようにしても、入れ子にはならない。
console.log(asyncFunc()); // Promise { "SUCCESS" }
const asyncFunc = async () => {
  return Promise.resolve("SUCCESS"); // Promise { "SUCCESS" }
};

// Promiseオブジェクトを返却するようにしても、入れ子にはならない。
console.log(asyncFunc()); // Promise { "SUCCESS" }


await宣言

▼ await宣言とは

非同期処理の結果をthenメソッドに渡す。

Promiseオブジェクトのthenメソッドに相当するが、thenメソッドのようにメソッドチェーンする必要はなくなるため、可読性が高い。

時間のかかる非同期処理でこれを宣言すると、予期せず処理が流れてしまうことを防げる。

また、await宣言により、コールバック地獄のコードが分かりやすくなる。

*実装例*

// Promiseオブジェクトのthenメソッドを使用した場合
const asyncFunc = async () => {
  axios.get("https://example.com").then((response) => {
    console.log(response.data);
  });
};

// awaitを使用した場合
const asyncFunc = async () => {
  // 非同期処理の結果がthenメソッドに渡される。
  const response = await axios.get("https://example.com");

  console.log(response.data);
};

*実装例*

// Promiseオブジェクトのthenメソッドを使用した場合
const asyncFunc = async () => {
  // コールバック関数地獄になっている。
  axios.get("https://example1.com").then((response) => {
    const response1 = response;
    axios.get("https://example1.com").then((response) => {
      const response2 = response;
      console.log(response1.data + response2.data);
    });
  });
};

// awaitを使用した場合
const asyncFunc = async () => {
  const response1 = await axios.get("https://example1.com");

  const response2 = await axios.get("https://example2.com");

  console.log(response1.data + response2.data);
};

▼ そもそも非同期にしなければawait宣言は不要なのでは?

関数を非同期処理化しなければ、await宣言がそもそも不要なのではという疑問がある。

例えば、通信処理を非同期処理化し、後続の通信処理結果によらない他の処理 (例:UIの更新) を実行しておく。

一方で、ファイル操作であれば、後続の処理は先行の処理結果が必要になるため、同期処理が適している。

ただ、使用するパッケージの仕様が非同期処理になっている場合 (例:Node.js上で使用できるJavaScriptには非同期処理の関数が多い) 、await宣言を使用せざるを得ない。


try-catch

▼ try-catchとは

Promiseオブジェクトのthenメソッド、catchメソッド、finallyメソッドを使用してエラーハンドリングを実装できるが、try-catch文とawait宣言を組み合わせて、より可読性高く実装できる。

*実装例*

const asyncFunc = async () => {
  return axios
    .get("https://example1.com")
    .catch((error) => {
      console.error(error);
    })
    .then((data) => {
      console.info(data);
    });
};
const asyncFunc = async () => {
  // 初期化
  let res;

  try {
    response = await axios.get("https://example1.com");
    console.info(response.data);
  } catch (error) {
    console.error(error);
  }

  return res;
};


async-retry

▼ async-retryとは

asyncによる非同期処理を再試行する。

await宣言した関数をretry関数に渡す。

const response = await retry(
  // 非同期処理
  async (values) => {},
);

*実装例*

// Packages
const retry = require("async-retry");
const fetch = require("node-fetch");

const response = await retry(
  // 対象の関数
  async (bail, num) => {
    const response = await fetch("https://google.com");

    if (403 === response.status) {
      // 403のときは再試行しない
      bail(new Error("Unauthorized"));
      return;
    }

    return await response.text();
  },
  // オプション
  {
    // 最大再試行回数
    retries: 10,
    // 指数関数的バックオフのfactor
    factor: 2,
    // 初回の待ち時間
    minTimeout: 1000,
    // 最大の待ち時間
    maxTimeout: Infinity,
    // ランダム化時の係数(1~2)
    randomize: true,
    // 再試行時に呼ばれる関数
    onRetry: (err, num) => console.error(err, num),
  },
);

console.log(response);


04. JQuery Promise

JQuery Promiseとは

JQueryパッケージの提供する独自のPromiseオブジェクトである。


doneメソッド、failメソッド、alwaysメソッド

JQuery Promiseオブジェクトが持つメソッド。

ajaxメソッドによってレスポンスを受信した後、その結果をdonefailalways3個に分類し、これに応じたコールバック処理を実行する。

*実装例*

JQueryパッケージのgetメソッドやpostメソッドを使用した場合。

const url = "https://www.google.co.jp/";

$.get(url)
  .done((data) => {
    console.info(data);
  })
  .fail((error) => {
    console.error(error);
  });
const url = "https://www.google.co.jp/";

const params = {
  name: "Hiroki",
};

$.post(url, params)
  .done((data) => {
    console.info(data);
  })
  .fail((error) => {
    console.error(error);
  });

JQueryパッケージのajaxメソッドを使用した場合。

const id = 1;

$.ajax({
  type: "POST",
  url: "/xxx/xxx/" + id + "/",
  contentType: "application/json",
  data: {
    param1: "AAA",
    param2: "BBB",
  },
})
  // 非同期通信の成功時のコールバック処理
  .done((data) => {
    console.info(data);
  })

  // 非同期通信の失敗時のコールバック処理
  .fail((error) => {
    console.info(data);
    toastr.error("", "エラーが発生しました。");
  })

  // 非同期通信の成功失敗に関わらず常に実行する処理
  .always((data) => {
    this.isLoaded = false;
  });


thenメソッド

JQuery Promiseオブジェクトが持つメソッド。

ajaxメソッドによってレスポンスを受信した後、その結果をthenメソッドの引数の順番で分類し、これに応じたコールバック処理を実行する。

非同期処理の後に同期処理を行いたい場合に使用する。

*実装例*

JQueryパッケージのajaxメソッドを使用した場合。

const id = 1;

$.ajax({
  type: "POST",
  url: "/xxx/xxx/" + id + "/",
  contentType: "application/json",
  data: {
    param1: "AAA",
    param2: "BBB",
  },
})
  // 最初のthen
  .then(
    // 引数1つめは通信成功時のコールバック処理
    (data) => {},
    // 引数2つめは通信失敗時のコールバック処理
    (data) => {},
  )
  // 次のthen
  .then(
    // 引数1つめは通信成功時のコールバック処理
    (data) => {},
  );