【そのまま使えるサンプルコードあり】【JS】setTimeout の連鎖をきれいに書きたい時の便利な解決法

setTimeout でアニメーションなどの非同期処理を順に処理するコードを綺麗に書きたい時のお話です。

とりあえずsetTimeout をきれいに書き直したいという方は【本題】まですっ飛ばしてください。

非同期処理は便利ですが、複数の処理を順番に処理することに関して言えば、これはしばしば地獄のようなコードを生み出します。たとえば、これはある3つの処理を順に実行したい時のコードです。

task1(function(result1) {
  task2(result1, function(result2) {
    task3(result2, function(result3) {
      console.log(`result3 : ${result3}`);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

これで問題ないじゃないか、って思う人は……ここはセンスの問題ですが、まあいないと思います。JavaScriptデベロッパーガイドでも、この地獄みたいなコードは“Callback hell”と呼ばれて恐れられています(笑)。

ある程度慣れてる人ならだいたい誰でも考え付くことだとは思うんですが、こういう順番に処理したい非同期処理を、

task1().task2().task3();

みたいに雑に並べて書かせてほしい……みたいなことを考えると思うんです。

そういう機能がないはずはないんですよね。

Promise を使ってすっきりとしたコードを書く

Promiseで処理を連鎖させましょう!

https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Using_promises
task1()
.then(function(result1) {
  return task2(result1);
})
.then(function(result2) {
  return task3(result2);
})
.then(function(result3) {
  console.log(`result3 : ${result3}`);
})
.catch(failureCallback);

.then(次の処理)の連鎖で、容易にこれが実現できます。

次の処理に使いたい変数をreturnに含めることで、連鎖の中で値を次々に受け渡すことができます。

これのいいところは、まず何より実行順にコードを書くので読みやすいこと。もうひとつは、エラーをcatchした時の処理(failureCallback)の記述が、エラーの発生個所に関係なく最後のひとつだけで済むことです。

setTimeoutを使った処理の場合

では、setTimeout()を使ったCallback hellを見てみましょう。

setTimeout(() => {
  task1();
  setTimeout(() => {
    task2();
    setTimeout(() => {
      task3();
      setTimeout(() => {
        task4();
      }, 1000);
    }, 1000);
  }, 1000);
}, 1000);

見てるだけで嫌になってきますね。何秒待ってるんだっけ? とか、今どこを処理してるんだっけ? みたいなことが本当に分かり辛いです。

【本題】returnで一定時間待たせる

まず、待たせるためだけの関数waitを用意します。このコードはそのままコピペでも使えますので、使っていただいても結構です。

const wait = function (ms) {
  return new Promise(function (f) {
    setTimeout(f, ms);
  });
};
// ms = 待機時間(ミリ秒)

これで、Promise連鎖の中で.then関数の戻り値にwait(待機時間)をとることで、自由に処理を遅延させられます。

task1()
.then(
  task2();
  return wait(1000);
})
.then(
  task3();
  return wait(1000);
})
.then(
  task4();
  return wait(1000);
})
.catch(failureCallback);

こんな具合に、きれいに縦に流して書けます。待機時間を変えたければ引数を変えるだけでOKです。わかりやすい。

いっそのこと一行で書く?

細かい説明は省きますが、変数の受け渡しなどが特に必要ない非同期処理を連鎖させたい場合、

const task1 = function(){
  //something to do.
}

const task2 = function(){
  //you know what to do.
}

const task3 = function(){
  //I wanna do something.
}

こうやって括っておいて、

[task1, task2, task3].reduce((p, f) => p.then(f), Promise.resolve());

/*
 *  これで
 *  Promise.resolve().then(func1).then(func2).then(func3); と同等
 */

この一行だけで完結させられます。

読みやすさと天秤にかけて……といったところでしょうか。