IT戦記

プログラミング、起業などについて書いているプログラマーのブログです😚

for 文を setTimeout に変換する

for 文で 100 項目とか 1000 項目とかあるテストケースを処理するとブラウザが固まる。

こんなダイアログが表示されます。

ということで for 文を setTimeout や setInterval に変換する事で定期的にブラウザに処理を戻すことができる。

// ここでは console.log のところでログを取ってますが
// 通常は処理が入ります。

for (var i = 0; i < 3; i ++) {
    console.log('a' + i);
}

/*
 * 結果
 *  a0
 *  a1
 *  a2
 */

これをまず while 文に変換

var i = 0;
while (true) {
  if (!(i < 3)) break;
  console.log('a' + i);
  i ++;
}

/*
 * 結果
 *  a0
 *  a1
 *  a2
 */

で、 setTimeout に変換

var i = 0;
setTimeout(function a() {
  if (!(i < 3)) return;
  console.log('a' + i);
  i ++;
  setTimeout(a);
}, 10);

/*
 * 結果
 *  a0
 *  a1
 *  a2
 */

これってアトミック?

つまり、 for 文が何個ネストしてても変換できる?

今度はネストした for

for (var i = 0; i < 3; i ++) {
  console.log('a' + i);
  for (var j = 0; j < 3; j ++) {
    console.log('b' + i + j);
  }
}

/*
 * 結果
 * a0
 * b00
 * b01
 * b02
 * a1
 * b10
 * b11
 * b12
 * a2
 * b20
 * b21
 * b22
 */

さっきの方法で変換

var i = 0;
setTimeout(function a() {
  if (!(i < 3)) return;
  console.log('a' + i);
  var j = 0;
  setTimeout(function b() {
    if (!(j < 3)) return;
    console.log('b' + i + j);
    j ++;
    setTimeout(b);
  }, 10);
  i ++;
  setTimeout(a);
}, 10);

/*
 * 結果(失敗してますね><
 * a0
 * b10
 * a1
 * b21
 * b20
 * a2
 * b32
 * b31
 * b30
 * b32
 * b31
 * b32
 */

何故、失敗したのか

外側の setTimeout が先にガーッと終わっちゃってあとから 内側が実行されるから。(ワーカースレッドのキューを想像してください

じゃあ、こう変換すればいいんじゃね?

var i = 0;
setTimeout(function a() {
  if (!(i < 3)) return;
  console.log('a' + i);
  var j = 0;
  setTimeout(function b() {
    if (!(j < 3)) return setTimeout(a); // ここで外側の setTimeout を呼ぶ
    console.log('b' + i + j);
    j ++;
    setTimeout(b);
  }, 10);
  i ++;
}, 10);

/*
 * 結果
 * a0
 * b00
 * b01
 * b02
 * a1
 * b10
 * b11
 * b12
 * a2
 * b20
 * b21
 * b22
 */

うまくいったけど、この変換はうまくいかないんじゃない

for (var i = 0; i < 3; i ++) {
  console.log('a' + i);
  for (var j = 0; j < 3; j ++) {
    console.log('b' + i + j);
  }
  for (var k = 0; k < 3; k ++) {
    console.log('c' + i + k);
  }
}

// めんどくさくなったので以下結果省略

た、たしかし><

var i = 0;
setTimeout(function a() {
  if (!(i < 3)) return;
  console.log('a' + i);
  var j = 0;
  setTimeout(function b() {
    if (!(j < 3)) return setTimeout(a);
    console.log('b' + i + j);
    j ++;
    setTimeout(b);
  }, 10);
  var k = 0;
  setTimeout(function c() {
    if (!(k < 3)) return setTimeout(a);
    console.log('c' + i + k);
    k ++;
    setTimeout(c);
  }, 10);
  i ++;
}, 10);

ここで戦略を練る

  • 単純に二つのループがネストした例なら、内側の setTimeout を呼ぶ必要が無くなったら、外側の setTimeout を呼べば良かった。
  • しかし、内側のループが二つある場合。
    • a は b の setTimeout を呼んで
    • b は c の setTimeout を呼んで
    • c は a の setTimeout を呼ぶ必要がある
  • ということは setTimeout を呼ぶ必要がなくなったら「次の処理を進ませる」ための関数を実行すればいいんじゃないか。

こうか

var i = 0;
setTimeout(function a() {
  if (!(i < 3)) return;
  console.log('a' + i);
  var j = 0;
  var b = function() {
    if (!(j < 3)) return b.next();
    console.log('b' + i + j);
    j ++;
    setTimeout(b);
  };
  setTimeout(b);
  b.next = function() {
    var k = 0;
    var c = function() {
      if (!(k < 3)) return c.next();
      console.log('c' + i + k);
      k ++;
      setTimeout(c);
    };
    setTimeout(c);
    c.next = function() {
      i ++;
      setTimeout(a);
    };
  };
}, 10);

next とかがめんどくさいので setTimeout に引数を渡せるようにする関数を作る(Firefox は元々渡せるけど)

// こういう関数を作った
var to = function() {
  var f = Array.prototype.shift.apply(arguments);
  args = arguments;
  return setTimeout(function() { f.apply(null, args) }, 10);
};

var i = 0;
to(function a(fin) {
  if (!(i < 3)) return fin();
  console.log('a' + i);
  var j = 0;
  to(function b(fin) {
    if (!(j < 3)) return fin();
    console.log('b' + i + j);
    j ++;
    to(b, fin);
  }, function() {
    var k = 0;
    to(function c(fin) {
      if (!(k < 3)) return fin();
      console.log('c' + i + k);
      k ++;
      to(c, fin);
    }, function() {
      i ++;
      to(a, fin);
    });
  });
}, function() {
});

できた!

わーわーぱちぱち

というわけでおさらい

以下のような for 文があったら
for (var i = 0; i < 3; i ++) {
  // 処理 1
}
// 処理 2
こんな感じの setTimeout を改造した関数をつくる
// ↓これ
var to = function() {
  var f = Array.prototype.shift.apply(arguments);
  args = arguments;
  return setTimeout(function() { f.apply(null, args) }, 10);
};

for (var i = 0; i < 3; i ++) {
  // 処理 1
}
// 処理 2
for を以下のような while にする
var to = function() {
  var f = Array.prototype.shift.apply(arguments);
  args = arguments;
  return setTimeout(function() { f.apply(null, args) }, 10);
};

// ↓こんな while 
var i = 0;
while (true) {
  if (!(i < 3)) break;
  // 処理 1
  i ++;
}
// 処理 2
でこうする
var to = function() {
  var f = Array.prototype.shift.apply(arguments);
  args = arguments;
  return setTimeout(function() { f.apply(null, args) }, 10);
};

var i = 0;
to (function f(fin) {
  if (!(i < 3)) return fin();
  // 処理 1
  i ++;
  to (f, fin);
}, function() {
// 処理 2
});

まとめ

ね?簡単でしょう?(ボブ風に