1

I have some asynchronous problems. I'm working on an ECMAScript 6 object. It's a timer and I want to be able to restart during its countdown.

Here is my work:

export class Timer {
    constructor(sec){
        this.sec = sec;
        this.count = sec;
        this.running = false;
    }

    start() {
        this.running = true;
        this._run();
    }

    _run(){
        if(this.running){
            setTimeout(()=>{
                this.count --;
                console.log(this.count);
                if(this.count<0){
                    this.running = false;
                }
                this._run();
            }, 1000);
        }
    }

    restart(){
        this.running = false;
        /*
            Wait until _run() is done then :
        */
        this.count = this.sec;
        this.start();
    }
}

In the restart() function, how can I know when _run() has stopped running?

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Dany Boisvert
  • 13
  • 1
  • 3
  • 1
    You seem to already be maintaining the running state with the `running` field. Couldn't you just query it's state? In the `restart` method, instead of changing the `running` field to false, you could just check if that's already the case. – Carcigenicate Jul 17 '15 at 15:22
  • Do you only want to *know whether* it has stopped running, or do you really need to *await* it? – Bergi Jul 17 '15 at 15:37
  • Most of the time, running returns to true before setTimeout() is done. – Dany Boisvert Jul 17 '15 at 15:53
  • @Bergi Well start() can start a new recursive process of _run(). They will both run in same time. – Dany Boisvert Jul 17 '15 at 16:02
  • @DanyBoisvert: And that's a problem, because they're not really running "both". Rather, your single `Timer` instance is running at double speed :-) The cleaner solution would probably be to create a new instance. How are you using these actually? I currently can't see what they *do*, as they never emit any messages back to the caller. – Bergi Jul 17 '15 at 16:05
  • @DanyBoisvert: What I'm asking for is whether you want the `(re)start` calls to *queue up*, or whether you just want to *detect* if it is still running and handle that somehow (without starting anything). – Bergi Jul 17 '15 at 16:07
  • @Bergi I want the (re)start calls to queue up – Dany Boisvert Jul 17 '15 at 17:07
  • Then you'll have to build a queue (or a queue counter) that is pushed to when `.running` is still true, and do the restart when your `_run` has finished (i.e. instead of setting `running` to false) – Bergi Jul 17 '15 at 17:44
  • That should fix my needs. I will use the timer and see weather or not i'll need to track the _run() process. – Dany Boisvert Jul 17 '15 at 17:56

1 Answers1

2

An easier way to know if the timer is "running" is to perhaps use setInterval instead.

var interval = setInterval(() => updateTimer(), 10); // update every 10ms

It's running if interval is set

if (interval) // timer is running

Stop the timer

window.clearInterval(interval);
interval = null;
// timer is no longer "running"

Additional notes

Beware of creating timers that increment with a fixed value

In your code, you have

setTimeout(() => this.count--, 1000);

The intention is for you to decrement your count property once every second, but this is not the behavior you will be guaranteed.

Check out this little script

var state = {now: Date.now()};

function delta(now) {
  let delta = now - state.now;
  state.now = now;
  return delta;
}

setInterval(() => console.log(delta(Date.now())), 1000);

// Output
1002
1000
1004
1002
1002
1001
1002
1000

We used setInterval(fn, 1000) but the actual interval varies a couple milliseconds each time.

The problem is exaggerated if you do things like switch your browser's focus to a different tab, open a new tab, etc. Look at these more sporadic numbers

1005 // close to 1000 ms
1005 // ...
1004 // a little variance here
1004 // ...
1834 // switched focus to previous browser tab
1231 // let timer tab run in background for a couple seconds
1082 // ...
1330 // ...
1240 // ...
2014 // switched back to timer tab
1044 // switched to previous tab
2461 // rapidly switched to many tabs below
1998 // ...
2000 // look at these numbers...
1992 // not even close to the 1000 ms that we set for the interval
2021 // ...
1989 // switched back to this tab
1040 // ...
1003 // numbers appear to stabilize while this tab is in focus
1004 // ...
1005 // ...

So, this means you can't rely upon your setTimeout (or setInterval) function getting run once per 1000 ms. count will be decremented with much variance depending on a wide variety of factors.

To work around this, you need to use a delta. That means before each "tick" of your timer, you need to take a timestamp using Date.now. On the next tick, take a new timestamp and subtract your previous timestamp from the new one. That is your delta. Using this value, add it to the Timer's total ms to get the precise number of milliseconds the timer has been running for.

Then, all time-sensitive values will be a projection/calculation of the total accumulated ms.

In your case, say you have a count that starts at 10. If you want to count down by -1 each 1000 ms, you could do

function update() {
  // update totalMs
  this.totalMs += calculateDelta();
  // display count based on totalMS
  console.log("count %d", Math.ceil(this.count - this.totalMs/1000));
}

Here's a sample ES6 timer that implements a delta function that might help you

class Timer {
  constructor(resolution=1000, ms=0) {
    this.ms = ms
    this.resolution = resolution;
    this.interval = null;
  }
  delta(now) {
    let delta = now - this.now;
    this.now = now;
    return delta;
  }
  start() {
    this.now = Date.now();
    this.interval = window.setInterval(() => this.update(), this.resolution);
  }
  reset() {
    this.update();
    this.ms = 0;
  }
  stop() {
    this.update();
    window.clearInterval(this.interval);
    this.interval = null;
  }
  update() {
    this.ms += this.delta(Date.now());
    console.log("%d ms - %0.2f sec", this.ms, this.ms/1000);
  }
}

Create a new timer with a 50 ms "resolution". All this means is that the timer display is updated every 50 ms. You could set this value to anything and the timer will still keep an accurate value.

var t = new Timer(50);
t.start();

To simulate the reset, we can just create a one-off timeout so you can see the reset working

// in ~5 seconds, reset the timer once
setTimeout(() => t.reset(), 5000);

Here's a demonstration of pausing the timer

// in ~10 seconds, pause the timer
setTimeout(() => t.stop(), 10000);

And you can resume the timer, too

// in ~12 seconds, resume the timer (without reset)
setTimeout(() => t.start(), 12000);

You can start, stop, reset the timer as much as you like


Here's an the ES6 (above) transpiled to ES5 so you can see the code working in a runnable snippet. Open your console and click Run code snippet.

"use strict";

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Timer = (function () {
  function Timer() {
    var resolution = arguments.length <= 0 || arguments[0] === undefined ? 1000 : arguments[0];
    var ms = arguments.length <= 1 || arguments[1] === undefined ? 0 : arguments[1];

    _classCallCheck(this, Timer);

    this.ms = ms;
    this.resolution = resolution;
    this.interval = null;
  }

  Timer.prototype.delta = function delta(now) {
    var delta = now - this.now;
    this.now = now;
    return delta;
  };

  Timer.prototype.start = function start() {
    var _this = this;

    this.now = Date.now();
    this.interval = window.setInterval(function () {
      return _this.update();
    }, this.resolution);
  };

  Timer.prototype.reset = function reset() {
    this.update();
    this.ms = 0;
  };

  Timer.prototype.stop = function stop() {
    this.update();
    window.clearInterval(this.interval);
    this.interval = null;
  };

  Timer.prototype.update = function update() {
    this.ms += this.delta(Date.now());
    console.log("%d ms - %0.2f sec", this.ms, this.ms / 1000);
  };

  return Timer;
})();

var t = new Timer(50);
t.start();

// in ~5 seconds, reset the timer once
setTimeout(function () {
  return t.reset();
}, 5000);

// in ~10 seconds, pause the timer
setTimeout(function () {
  return t.stop();
}, 10000);

// in ~12 seconds, resume the timer (without reset)
setTimeout(function () {
  return t.start();
}, 12000);
Mulan
  • 129,518
  • 31
  • 228
  • 259