Dany Boisvert
Dany Boisvert

Reputation: 13

How to stop asynchronous function in JavaScript?

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?

Upvotes: 1

Views: 5373

Answers (1)

Mulan
Mulan

Reputation: 135197

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);

Upvotes: 2

Related Questions