この解答例はChatGPTによって生成されています,正しいかは自己判断で。

BFE.devでのAI解答例 - JavaScript コーディング問題
84. fake timer (setInterval)を実装する

class FakeTimer {
  install() {
    const originSetInterval = window.setInterval;
    const originClearInterval = window.clearInterval;
    const originDateNow = Date.now;

    let currentTime = 0;
    let intervalIdMap = new Map();

    window.setInterval = function (fn, delay) {
      let id = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
      const intervalFunc = () => {
        fn();
        intervalIdMap.set(id, originSetInterval(intervalFunc, delay));
      };
      intervalIdMap.set(id, originSetInterval(intervalFunc, delay));
      return id;
    };

    window.clearInterval = function (intervalId) {
      const interval = intervalIdMap.get(intervalId);
      if (interval) {
        originClearInterval(interval);
        intervalIdMap.delete(intervalId);
      }
    };

    Date.now = function () {
      return currentTime;
    };

    let previousTickTime = Date.now();

    this.tick = function (duration = 0) {
      const ticksCount = Math.ceil(duration / 10);
      const newCurrentTime = currentTime + duration;
      for (let i = 0; i < ticksCount; i++) {
        previousTickTime = currentTime;
        currentTime += 10;
        intervalIdMap.forEach((interval, id) => {
          if (currentTime - previousTickTime === interval.delay) {
            interval.func();
          }
        });
      }
    };
  }

  uninstall() {
    window.setInterval = originSetInterval;
    window.clearInterval = originClearInterval;
    Date.now = originDateNow;
  }

  tick() {
    this.tick(10);
  }
}

補足

  • setInterval 関数をトラッキングするために intervalIdMap という Map を定義して、 setInterval で生成した id をキー、interval id と interval function のオブジェクトを値として保存するようにしています。
  • setInterval をそれぞれの delay 毎に実行し、intervalIdMap を使って clearInterval を呼び出すには、以下のようにします。
    • id に対応する interval object を取得
    • 値を実行して、次の実行をスケジューリング
  • currentTime の変数は常に時間を格納し、 Date.now() への置き換え関数のために使用されています。
  • tick() 関数は指定された duration の時間だけイベントループを進めています。 tick() メソッド内のループでは、 intervalIdMap 内のすべての interval object をチェックして、currentTime - previousTickTime タイムアウトした場合に、オブジェクトの機能を実行しています。
  • random で生成された id がシステム上で一意であることを保証するために、 setInterval によって生成される整数の範囲を最大整数に制限しました。
let id = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);

テストの例

const fakeTimer = new FakeTimer();
fakeTimer.install();

const logs = [];
const log = () => {
  logs.push(Date.now());
};

let count = 0;
const id = setInterval(() => {
  if (count > 1) {
    clearInterval(id);
  } else {
    log();
  }
  count += 1;
}, 100);

// log 'A' at every 100, stop at 200
fakeTimer.tick(200);
fakeTimer.uninstall();

expect(logs).toEqual([100, 200]);

// Test for setInterval and clearInterval
const intervals = [];
const intervalsMap = new Map();

const callback = () => {
  console.log('Callback');
};

const createInterval = (cb, delay) => {
  const id = setInterval(cb, delay);
  intervals.push(id);
  intervalsMap.set(id, { cb, delay });
};

createInterval(callback, 100);
createInterval(callback, 1200);

fakeTimer.tick(2600);

intervals.forEach((intervalId) => clearInterval(intervalId));

const intervalsStats = [
  {
    intervalFn: () => console.log('Callback 1'),
    delay: 100,
    calls: 26
  },
  {
    intervalFn: () => console.log('Callback 2'),
    delay: 1200,
    calls: 2
  }
];

let callStats = intervalsStats.map((v) => ({ ...v, calls: 0 }));

for (let i = 0; i < callStats.length; i++) {
  let intervalInvocations = intervalsMap.get(intervals[i]);
  let intervalFn = intervalInvocations['cb'];
  let delay = intervalInvocations['delay'];
  for (let j = 0; j < callStats[i]['calls']; j++) {
    expect(logs.shift()).toBe(j * delay + (i === 0 ? 100 : 0));
  }
}