Функция таймера не работает правильно при остановке и запуске несколько раз

Прежде всего, вы можете найти пример моего кода в JS Fiddle, а также под вопрос.

Я работаю над персональным обучающим Webapp, и в основном вы можете играть в игру, а затем вы получаете пять минут, чтобы выполнять ряд задач в произвольном порядке. Программа создает массив sessionTasks, в который помещаются задачи случайного порядка для массива tasks, чтобы соответствовать пятиминутному ограничению. Прямо сейчас массив tasks - это только один, который я создал с четырьмя задачами, и соответствующие времена для тестирования.

Проблема, с которой я столкнулся, заключается в следующем: когда вы нажимаете задачу, чтобы перейти к следующей задаче, следующий раз, когда вы будете играть секунды, будет двигаться быстрее. То, как я нашел для репликации:

  • Нажмите кнопку воспроизведения.
  • Вы можете быстро выполнить задачи, щелкнув текст задачи.
  • Повторно нажмите кнопку воспроизведения.

Теперь секунды должны двигаться быстрее. Если нет, повторите то, что вы только что сделали. Это нерегулярно, но обычно это делается во второй попытке.

Я не могу в жизни понять, почему так ведет себя. Я думал, что, возможно, это создавало больше таймеров, которые все использовали #taskTimer для запуска, но это не имело смысла для меня. Это что-то не так с функцией Timer? Что не так в моем коде?

mainMenu();


var totalSessionTasks, taskIterator, selectedTimeInSecs = 300;

var taskTimer = new Timer("#taskTimer", nextTask);

var globalTimer = new Timer("#globalTimer", function() {

});

var tasks = [
  ["First task", 0, 30],
  ["Second task", 0, 15],
  ["Third task", 0, 10],
  ["Fourth task", 3, 0]
];

var sessionTasks = [

]




function setUpSession() {

  sessionTasks = []

  if (tasks.length != 0) {

    var sessionTasksSeconds = 0; //the seconds of the session being filled
    var sessionTasksSecondsToFill = selectedTimeInSecs; //seconds left in the session to fill
    var newTaskSeconds = 0; //seconds of the next task being added to the session
    var sessionFull = false;

    console.log('Session Empty');

    while (sessionFull === false) {

      var areThereAnyTaskThatFitInTheSession =
        tasks.some(function(item) {
          return ((item[1] * 60 + item[2]) <= sessionTasksSecondsToFill) && (item != sessionTasks[sessionTasks.length - 1]);
        });
      console.log(areThereAnyTaskThatFitInTheSession);

      if (areThereAnyTaskThatFitInTheSession) {
        do {
          var randTaskNum = Math.floor(Math.random() * tasks.length);
        } while (((tasks[randTaskNum][1] * 60 + tasks[randTaskNum][2]) > sessionTasksSecondsToFill) || (tasks[randTaskNum] == sessionTasks[sessionTasks.length - 1]))

        sessionTasks.push(tasks[randTaskNum]);
        newTaskSeconds = (tasks[randTaskNum][1]) * 60 + tasks[randTaskNum][2];
        sessionTasksSecondsToFill -= newTaskSeconds;
        sessionTasksSeconds += newTaskSeconds;

        console.log(tasks[randTaskNum][0] + ": " + newTaskSeconds + "s");
        console.log(sessionTasksSeconds)

      } else if (sessionTasks.length == 0) {
        note("All your tasks are too big for a game of " + selectedTimeInSecs / 60 + " minutes!");
        break;
      } else {
        console.log('Session full');
        sessionFull = true;
        taskIterator = -1;
        totalSessionTasks = sessionTasks.length;
        console.log(totalSessionTasks);

        globalTimer.set(0, sessionTasksSeconds);
        nextTask();
        globalTimer.run();
        taskTimer.run();
      }


    }

  } else {
    note("You don't have have any tasks in your playlists!");
  }

}


function nextTask() {

  if (taskIterator + 1 < totalSessionTasks) {
    taskIterator++;
    $("#taskText").text(sessionTasks[taskIterator][0]);
    globalTimer.subtract(0, taskTimer.getTotalTimeInSeconds())
    taskTimer.set(sessionTasks[taskIterator][1], sessionTasks[taskIterator][2]);
    $("#taskCounter").text(taskIterator + 1 + " of " + totalSessionTasks + " tasks");
  } else {
    mainMenu();
    taskTimer.stop();
    globalTimer.stop();
    note("Thanks for playing!");
  }


}

//timer object function
function Timer(element, callback) {

  var ac, minutes, seconds, finalTimeInSeconds, displayMinutes, displaySeconds, interval = 1000,
    self = this,
    timeLeftToNextSecond = 1000;
  this.running = false;

  this.set = function(inputMinutes, inputSeconds) {

    finalTimeInSeconds = inputMinutes * 60 + inputSeconds;
    minutes = (Math.floor(finalTimeInSeconds / 60));
    seconds = finalTimeInSeconds % 60;

    this.print();
  }

  this.add = function(inputMinutes, inputSeconds) {

    finalTimeInSeconds += inputMinutes * 60 + inputSeconds;
    finalTimeInSeconds = (finalTimeInSeconds < 0) ? 0 : finalTimeInSeconds;
    minutes = (Math.floor(finalTimeInSeconds / 60));
    seconds = finalTimeInSeconds % 60;

    this.print();
  }

  this.subtract = function(inputMinutes, inputSeconds) {

    finalTimeInSeconds -= inputMinutes * 60 + inputSeconds;
    if (finalTimeInSeconds <= 0) {
      callback()
    }
    finalTimeInSeconds = (finalTimeInSeconds < 0) ? 0 : finalTimeInSeconds;
    minutes = (Math.floor(finalTimeInSeconds / 60));
    seconds = finalTimeInSeconds % 60;
    this.print();
  }

  this.reset = function() {

    this.set(0, 0);
  }

  this.print = function() {

    displayMinutes = (minutes.toString().length == 1) ? "0" + minutes : minutes; //ternary operator: adds a zero to the beggining 
    displaySeconds = (seconds.toString().length == 1) ? "0" + seconds : seconds; //of the number if it has only one caracter.

    $(element).text(displayMinutes + ":" + displaySeconds);
  }

  this.run = function() {

    if (this.running == false) {
      this.running = true;

      var _f = function() {
        secondStarted = new Date;
        self.subtract(0, 1);
        interval = 1000;
      }
      ac = setInterval(_f, interval);


    }
  }

  this.stop = function() {

    if (this.running == true) {
      this.running = false;
      console.log(this + "(" + element + ") was stopped");
      stopped = new Date;
      interval = 1000 - (stopped - secondStarted);
      clearInterval(ac);
    }
  }

  this.getTotalTimeInSeconds = function() {


    return finalTimeInSeconds;
  }

  this.reset();

}

function note(string) {
  alert(string);
}

function mainMenu() {
  //EMPTY BODY
  $("body").empty();
  $("body").append(
    //BUTTONS
    "<div id='playButton' class='mainButton'><div class='buttonText mainButtonText'>PLAY</div></div>"
  );
  //BINDS
  $("#playButton").bind("click", function(){
  	playMain();
    setUpSession();
  });

}

function playMain() {
  //EMPTY BODY
  $("body").empty();
  $("body").append(
    //TASK TEXT
    "<p class='text' id='taskText'>Lorem ipsum dolor sit amet.</p>",
    //TIMERS
    "<div id='taskTimerWrap'><p class='text timer' id='taskTimer'>00:00</p><p class='text' id='taskTimerText'>Task Time</p></div>",
    "<div id='globalTimerWrap'><p class='text timer' id='globalTimer'>00:00</p><p class='text' id='globalTimerText'>Global Time</p></div>",
    //TASK COUNTER
    "<div class='text' id='taskCounter'>0/0 tasks completed</div>"
  );
  //BINDS
  $("#taskText").bind("click", nextTask);
}
#taskText {
  text-align: center;
  display: table;
  vertical-align: middle;
  height: auto;
  width: 100%;
  top: 50px;
  bottom: 0;
  left: 0;
  right: 0;
  position: absolute;
  margin: auto;
  font-size: 65px;
  cursor: pointer;
}

#taskTimerWrap {
  text-align: center;
  top: 0;
  right: 0;
  left: 170px;
  margin: 5px;
  position: absolute;
  -webkit-transition: all 0.5s ease;
}

.timer {
  font-size: 64px;
  margin: 0;
  line-height: 0.88;
}

#taskTimerText {
  font-size: 34.4px;
  margin: 0;
  line-height: 0.65;
}

#globalTimerWrap {
  text-align: center;
  top: 0;
  left: 0;
  right: 170px;
  margin: 5px;
  position: absolute;
}

#globalTimerText {
  font-size: 28.5px;
  margin: 0;
  line-height: 0.78;
  transform: scale(1, 1.2);
}

#taskCounter {
  text-align: center;
  bottom: 0;
  right: 0;
  left: 0;
  width: auto;
  position: absolute;
  font-size: 30px;
  color: #98D8D9;
  -webkit-transition: all 0.5s ease;
}

#taskCounter:hover {
  color: #F1F2F0
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

Ответы

Ответ 1

В Timer.stop() вы изменяете интервал, используемый для следующего прогона. Изменение переменной интервала в _f() не изменит интервал, используемый setInterval().

В этом случае вам придется использовать setTimeout() вместо:

function Timer(element, callback) {    
  var ac, minutes, seconds, finalTimeInSeconds, displayMinutes, displaySeconds, timeout = 1000,
    self = this,
    timeLeftToNextSecond = 1000;
  this.running = false;

  /* ... */

  this.run = function() {
    if (this.running == false) {
      this.running = true;

      var _f = function() {
        secondStarted = new Date;
        self.subtract(0, 1);        
        ac = setTimeout(_f, 1000);
      }
      ac = setTimeout(_f, timeout);
    }
  }

  this.stop = function() {    
    if (this.running == true) {
      this.running = false;
      console.log(this + "(" + element + ") was stopped");
      stopped = new Date;
      timeout = 1000 - (stopped - secondStarted);
      clearTimeout(ac);
    }
  }

  /* ... */    
}

Ответ 2

Вот обновление в jsfiddle

Проблема заключается в переменной interval of Timer

Эта строка создает непредсказуемый интервал под Timer.stop():

interval = 1000 - (stopped - secondStarted);

Если вы хотите сократить временной интервал, вы можете добавить свойство play_count:

    ...

    //timer object function
    function Timer(element, callback) {

  var ac, minutes, seconds, finalTimeInSeconds, displayMinutes, displaySeconds, interval = 1000,
    self = this,
    timeLeftToNextSecond = 1000;

  this.running = false;
 play_count = 0;  // play count property

...

  this.run = function() {

    if (this.running == false) {
      this.running = true;

      var _f = function() {
        secondStarted = new Date;
        self.subtract(0, 1);
        interval = Math.max(1000 - play_count * 100, 500);  // ** <-- shorten time interval
      }
      ac = setInterval(_f, interval);


    }
  }

  this.stop = function() {

    if (this.running == true) {
      this.running = false;
      console.log(this + "(" + element + ") was stopped");
//      stopped = new Date;
//      interval = 1000 - (stopped - secondStarted);
            play_count++;
      clearInterval(ac);
    }
  }

  this.getTotalTimeInSeconds = function() {


    return finalTimeInSeconds;
  }

  this.reset();

}
...

Ответ 3

Как я вижу, вы пытаетесь установить значение переменной interval внутри функции обратного вызова интервала.

      var _f = function() {
        secondStarted = new Date;
        self.subtract(0, 1);
        //interval = 1000; REMOVE THIS LINE
      }
      interval = 1000; //ADD IT HERE
      ac = setInterval(_f, interval);

Фактически, когда setInterval выполняется, значение для interval не 1000. Он еще не обновлен _f, потому что эта функция будет запущена после выполнения setInterval(). И как только setInterval() вызывается с существующим значением interval, изменение его позже не влияет на созданный интервал, потому что функция setInterval() уже установила время задержки для созданного интервала. И значение для interval изменяет из него начальное значение 1000 из-за этой строки:

 interval = 1000 - (stopped - secondStarted); //I'm not sure what you are trying to do with this, possibly removing this line will also fix your problem.)

Полная рабочая демонстрация:

И вот JS Fiddle.

mainMenu();


var totalSessionTasks, taskIterator, selectedTimeInSecs = 300;

var taskTimer = new Timer("#taskTimer", nextTask);

var globalTimer = new Timer("#globalTimer", function() {

});

var tasks = [
  ["First task", 0, 30],
  ["Second task", 0, 15],
  ["Third task", 0, 10],
  ["Fourth task", 3, 0]
];

var sessionTasks = [

]




function setUpSession() {

  sessionTasks = []

  if (tasks.length != 0) {

    var sessionTasksSeconds = 0; //the seconds of the session being filled
    var sessionTasksSecondsToFill = selectedTimeInSecs; //seconds left in the session to fill
    var newTaskSeconds = 0; //seconds of the next task being added to the session
    var sessionFull = false;

    console.log('Session Empty');

    while (sessionFull === false) {

      var areThereAnyTaskThatFitInTheSession =
        tasks.some(function(item) {
          return ((item[1] * 60 + item[2]) <= sessionTasksSecondsToFill) && (item != sessionTasks[sessionTasks.length - 1]);
        });
      console.log(areThereAnyTaskThatFitInTheSession);

      if (areThereAnyTaskThatFitInTheSession) {
        do {
          var randTaskNum = Math.floor(Math.random() * tasks.length);
        } while (((tasks[randTaskNum][1] * 60 + tasks[randTaskNum][2]) > sessionTasksSecondsToFill) || (tasks[randTaskNum] == sessionTasks[sessionTasks.length - 1]))

        sessionTasks.push(tasks[randTaskNum]);
        newTaskSeconds = (tasks[randTaskNum][1]) * 60 + tasks[randTaskNum][2];
        sessionTasksSecondsToFill -= newTaskSeconds;
        sessionTasksSeconds += newTaskSeconds;

        console.log(tasks[randTaskNum][0] + ": " + newTaskSeconds + "s");
        console.log(sessionTasksSeconds)

      } else if (sessionTasks.length == 0) {
        note("All your tasks are too big for a game of " + selectedTimeInSecs / 60 + " minutes!");
        break;
      } else {
        console.log('Session full');
        sessionFull = true;
        taskIterator = -1;
        totalSessionTasks = sessionTasks.length;
        console.log(totalSessionTasks);

        globalTimer.set(0, sessionTasksSeconds);
        nextTask();
        globalTimer.run();
        taskTimer.run();
      }


    }

  } else {
    note("You don't have have any tasks in your playlists!");
  }

}


function nextTask() {

  if (taskIterator + 1 < totalSessionTasks) {
    taskIterator++;
    $("#taskText").text(sessionTasks[taskIterator][0]);
    globalTimer.subtract(0, taskTimer.getTotalTimeInSeconds())
    taskTimer.set(sessionTasks[taskIterator][1], sessionTasks[taskIterator][2]);
    $("#taskCounter").text(taskIterator + 1 + " of " + totalSessionTasks + " tasks");
  } else {
    mainMenu();
    taskTimer.stop();
    globalTimer.stop();
    note("Thanks for playing!");
  }


}

//timer object function
function Timer(element, callback) {

  var ac, minutes, seconds, finalTimeInSeconds, displayMinutes, displaySeconds, interval = 1000,
    self = this,
    timeLeftToNextSecond = 1000;
  this.running = false;

  this.set = function(inputMinutes, inputSeconds) {

    finalTimeInSeconds = inputMinutes * 60 + inputSeconds;
    minutes = (Math.floor(finalTimeInSeconds / 60));
    seconds = finalTimeInSeconds % 60;

    this.print();
  }

  this.add = function(inputMinutes, inputSeconds) {

    finalTimeInSeconds += inputMinutes * 60 + inputSeconds;
    finalTimeInSeconds = (finalTimeInSeconds < 0) ? 0 : finalTimeInSeconds;
    minutes = (Math.floor(finalTimeInSeconds / 60));
    seconds = finalTimeInSeconds % 60;

    this.print();
  }

  this.subtract = function(inputMinutes, inputSeconds) {

    finalTimeInSeconds -= inputMinutes * 60 + inputSeconds;
    if (finalTimeInSeconds <= 0) {
      callback()
    }
    finalTimeInSeconds = (finalTimeInSeconds < 0) ? 0 : finalTimeInSeconds;
    minutes = (Math.floor(finalTimeInSeconds / 60));
    seconds = finalTimeInSeconds % 60;
    this.print();
  }

  this.reset = function() {

    this.set(0, 0);
  }

  this.print = function() {

    displayMinutes = (minutes.toString().length == 1) ? "0" + minutes : minutes; //ternary operator: adds a zero to the beggining 
    displaySeconds = (seconds.toString().length == 1) ? "0" + seconds : seconds; //of the number if it has only one caracter.

    $(element).text(displayMinutes + ":" + displaySeconds);
  }

  this.run = function() {

    if (this.running == false) {
      this.running = true;

      var _f = function() {
        secondStarted = new Date;
        self.subtract(0, 1);
        //interval = 1000; REMOVE THIS LINE
      }
      interval = 1000; //ADD IT HERE
      ac = setInterval(_f, interval);


    }
  }

  this.stop = function() {

    if (this.running == true) {
      this.running = false;
      console.log(this + "(" + element + ") was stopped");
      stopped = new Date;
      interval = 1000 - (stopped - secondStarted);
      clearInterval(ac);
    }
  }

  this.getTotalTimeInSeconds = function() {


    return finalTimeInSeconds;
  }

  this.reset();

}

function note(string) {
  alert(string);
}

function mainMenu() {
  //EMPTY BODY
  $("body").empty();
  $("body").append(
    //BUTTONS
    "<div id='playButton' class='mainButton'><div class='buttonText mainButtonText'>PLAY</div></div>"
  );
  //BINDS
  $("#playButton").bind("click", function(){
  	playMain();
    setUpSession();
  });

}

function playMain() {
  //EMPTY BODY
  $("body").empty();
  $("body").append(
    //TASK TEXT
    "<p class='text' id='taskText'>Lorem ipsum dolor sit amet.</p>",
    //TIMERS
    "<div id='taskTimerWrap'><p class='text timer' id='taskTimer'>00:00</p><p class='text' id='taskTimerText'>Task Time</p></div>",
    "<div id='globalTimerWrap'><p class='text timer' id='globalTimer'>00:00</p><p class='text' id='globalTimerText'>Global Time</p></div>",
    //TASK COUNTER
    "<div class='text' id='taskCounter'>0/0 tasks completed</div>"
  );
  //BINDS
  $("#taskText").bind("click", nextTask);
}
#taskText {
  text-align: center;
  display: table;
  vertical-align: middle;
  height: auto;
  width: 100%;
  top: 50px;
  bottom: 0;
  left: 0;
  right: 0;
  position: absolute;
  margin: auto;
  font-size: 65px;
  cursor: pointer;
}

#taskTimerWrap {
  text-align: center;
  top: 0;
  right: 0;
  left: 170px;
  margin: 5px;
  position: absolute;
  -webkit-transition: all 0.5s ease;
}

.timer {
  font-size: 64px;
  margin: 0;
  line-height: 0.88;
}

#taskTimerText {
  font-size: 34.4px;
  margin: 0;
  line-height: 0.65;
}

#globalTimerWrap {
  text-align: center;
  top: 0;
  left: 0;
  right: 170px;
  margin: 5px;
  position: absolute;
}

#globalTimerText {
  font-size: 28.5px;
  margin: 0;
  line-height: 0.78;
  transform: scale(1, 1.2);
}

#taskCounter {
  text-align: center;
  bottom: 0;
  right: 0;
  left: 0;
  width: auto;
  position: absolute;
  font-size: 30px;
  color: #98D8D9;
  -webkit-transition: all 0.5s ease;
}

#taskCounter:hover {
  color: #F1F2F0
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>