JavaScript 入門 非同期処理/コールバック/Promise なんとなくイメージする

今回は非同期処理、コールバック、Promise について、なぜ・どのように使うのかをなんとなくイメージできるような実例を記載しました。

非同期

JavaScript で以下のプログラムを書いてみます。

  • X秒後に値Y返す処理を書く
  • 値Yに+1した結果をコンソールに表示する

XとYを引数にとる関数を書き、それを実行した結果に +1 したものをコンソールに表示します。

// ms はミリセカンド
function hoge(ms, value){
	setTimeout(() => {
  	return value
  }, ms);
}

const result = hoge(2000, 2);
console.log(result + 1);

これを実行すると NaN と表示されてしまいます。想定した結果になりません。

実は const result = hoge(2000, 2); の処理を実行すると、2秒待たずに次の行の console.log(result + 1); が実行されます。

そのため、result は undefined なので undefined + 1 の計算をしてしまい、NaN となります。

時間がかかる処理があった場合に、その処理の完了を待たずに次の処理に移ってしまうので、普通に書いてしまうと上記のような結果になってしまいます。

setTimeout を変更してみる

まず、単純な解決方法としては、setTimeout の中に処理をいれてしまうことです。

function hoge(ms, value){
	setTimeout(() => {
  	console.log(value + 1);
  }, ms);
}

hoge(2000, 2);

これでも良いのですが問題があります。

hoge の役割が変わってしまっている、ということです。

hoge の役割はあくまでX秒後にY値を返す、というだけにしていたのですが、上のようにやってしまうと以下のように変わってしまいます。

  • X秒後にY+1の値をコンソールに表示する

このようにしてしまうと、X秒後にY+2したい、という処理がでてきた場合に同じような処理を書かないといけないです。

function hoge(ms, value){
	setTimeout(() => {
  	console.log(value + 1);
  }, ms);
}

// hoge2という新しい関数を作ってみた。。。
function hoge2(ms, value){
	setTimeout(() => {
  	console.log(value + 2);
  }, ms);
}

hoge(2000, 2);
hoge2(2000, 2);

コードの重複はバグの温床になります。

できれば、hoge の役割はシンプルにして「X秒後に値Y返す」だけにして、その後の加工処理は別にした方が応用がききます。

コールバックを使って重複をなくしてみる

hoge と hoge2 はほぼ同じことをしています。唯一違うのはY値に対して何をするかです。

このような場合はどうしたら良いでしょうか。

本ブログでも何度か記載しましたが、動的に変わる部分は関数の引数で渡してしまいましょう。

hoge の第3引数にコールバックを指定します。コールバックは関数でX秒後に実行される処理 です。

X秒後に何をしたいかを動的に変更できるので、hoge2 は必要なくなります。

function hoge(ms, value, callback){
	setTimeout(() => {
  	callback(value);
  }, ms);
}
function myCallback(value){
	console.log(value + 1);
}
function myCallback2(value){
	console.log(value + 2);
}

hoge(2000, 2, myCallback);
hoge(2000, 2, myCallback2);

これで hoge と hoge2 の重複は消えました。

ただ、hoge の役割が「X秒後に、コールバックにY値を渡した処理を行う」となって、やはり当初の役割からずれています。

Promise を使ったアプローチ

別の方法を考えてみます。

元々の役割だった「X秒後に値Y返す」のままで目的を果たすにはどうすれば良いでしょうか。

Promise を使ってみます。

function hoge(ms, value) {
	return new Promise((resolve, reject) => {
  	setTimeout(() => {
    	resolve(value);
    }, ms);
  });
}

hoge(2000, 2).then(v => console.log(v + 1));
hoge(2000, 2).then(v => console.log(v + 2));

hoge から第3引数が消え、単純に「X秒後に値Y返す」という処理になりました。

そして、+1してコンソールに表示する、+2してコンソールに表示する、という動的に変わる部分も実現できています。

二つのアプローチの違い

コールバックの方法をとったときは、hoge という関数には「X秒後に何をするか」という責務が含まれていました。

Promise を使うと、「X秒後に何をするか」の部分が hoge 内から完全に切り離されました。

切り離されていない場合は、当然ですが「X秒後にする処理」が複雑化したときに hoge が影響を受けます。

例えば、コールバック関数の引数が二つになった場合。

function hoge(ms, value, callback, add){
	setTimeout(() => {
  	callback(value, add);
  }, ms);
}
function myCallback(value, add){
	console.log(value + add);
}

hoge(2000, 2, myCallback, 1);
hoge(2000, 2, myCallback, 2);

hoge の引数も追加しないといけないので、どんどん hoge が肥大化していきます。

一方で、Promise の場合は完全に責務が分離されているので、コールバックの処理がどうなろうと hoge には影響はありません。

function hoge(ms, value) {
	return new Promise((resolve, reject) => {
  	setTimeout(() => {
    	resolve(value);
    }, ms);
  });
}

function myFunc(value, add) {
	console.log(value + add);
}

hoge(2000, 2).then(v => myFunc(v, 1));
hoge(2000, 2).then(v => myFunc(v, 2));