6

I understand how to use setTimeout function, but I can't find a way to create a function like it.
I have an example:

setTimeout(() => {
  console.log('3s');
}, 3000);
while(1);

The result is setTimeout callback never call so I think it use the same thread like every js other functions. But when it's check the time reach or not? and how it can do that?

Updated

To avoid misunderstanding I update my question.
I can't find a way to create a async function with callback after specify time (without using setTimeout and don't block entire thread). This function setTimeout seen like a miracle to me. I want to understand how it work.

4
  • 4
    You can't emulate setTimeout without using timeout. Commented May 24, 2018 at 5:06
  • @Teemu thank for your help, so this function is some kind of special function that we can't emulate it? Commented May 24, 2018 at 5:09
  • Why do you have while(1) in there? Commented May 24, 2018 at 5:13
  • 1
    It's much like an event which uses the in-built event queue (or something similar), you'd need to emulate the queue, and doing that wouldn't be very efficient. Commented May 24, 2018 at 5:13

5 Answers 5

10

Just for the game since I really don't see why you couldn't use setTimeout...


To create a non-blocking timer, without using the setTimeout/setInterval methods, you have only two ways:

  • event based timer
  • run your infinite loop in a second thread

Event based timer

One naive implementation would be to use the MessageEvent interface and polling until the time has been reached. But that's not really advice-able for long timeouts as this would force the event-loop to constantly poll new tasks, which is bad for trees.

function myTimer(cb, ms) {
  const begin = performance.now();
  const channel = myTimer.channel ??= new MessageChannel();
  const controller = new AbortController();
  channel.port1.addEventListener("message", (evt) => {
    if(performance.now() - begin >= ms) {
      controller.abort();
      cb();
    }
    else if(evt.data === begin) channel.port2.postMessage(begin);
  }, { signal: controller.signal });
  channel.port1.start();
  channel.port2.postMessage(begin);
}

myTimer(() => console.log("world"), 2000);
myTimer(() => console.log("hello"), 100);

So instead, if available, one might want to use the Web Audio API and the AudioScheduledSourceNode, which makes great use of the high precision Audio Context's own clock:

function myTimer(cb, ms) {
  if(!myTimer.ctx) myTimer.ctx = new (window.AudioContext || window.webkitAudioContext)();
  var ctx = myTimer.ctx;
  var silence = ctx.createGain();
  silence.gain.value = 0;
  var note = ctx.createOscillator();
  note.connect(silence);
  silence.connect(ctx.destination);
  note.onended = function() { cb() };
  note.start(0);
  note.stop(ctx.currentTime + (ms / 1000));
}

myTimer(()=>console.log('world'), 2000);
myTimer(()=>console.log('hello'), 200);

Infinite loop on a different thread

Yes, using Web Workers we can run infinite loops without killing our web page:

function myTimer(cb, ms) {
  var workerBlob = new Blob([mytimerworkerscript.textContent], {type: 'application/javascript'});
  var url = URL.createObjectURL(workerBlob);
  var worker = new Worker(url);
  worker.onmessage = function() {
    URL.revokeObjectURL(url);
    worker.terminate();
    cb();
  };
  worker.postMessage(ms);
}

myTimer(()=>console.log('world'), 2000);
myTimer(()=>console.log('hello'), 200);
<script id="mytimerworkerscript" type="application/worker-script">
  self.onmessage = function(evt) {
    var ms = evt.data;
    var now = performance.now();
    while(performance.now() - now < ms) {}
    self.postMessage('done');
  }
</script>

And for the ones who like to show off they know about the latest features not yet really available (totally not my style), a little mention of the incoming Prioritized Post Task API and its delayed tasks, which are basically a more powerful setTimeout, returning a promise, on which we can set prioritization.

(async () => {
  if(globalThis.scheduler) {
    const p1 = scheduler.postTask(()=>{ console.log("world"); }, { delay: 2000} );
    const p2 = scheduler.postTask(()=>{ console.log("hello"); }, { delay: 1000} );
    await p2;
    console.log("future");
  }
  else {
    console.log("Your browser doesn't support this API yet");
  }
})();

Sign up to request clarification or add additional context in comments.

3 Comments

Actually this is helpful, if you need a custom one. Thanks Kaiido :)
your EVENT based timer hangs if there are just 2(or more) timers that are yet to be resolved, however the audio one works very smoothly
@TheBombSquad it's expected that this solution is the worst in terms of performance and it may indeed slow down the main scripts, but it shouldn't block it though. Anyway, I updated it to use MessageChannels instead of polluting the window's message event (I suspect it did block for you because an other script, maybe from an extension, was also listening to these messages).
4

The reason callback of setTimeout() is not being called is, you have while(1) in your code which acts as infinite loop. It will keep your javascript stack busy whole time and that is the reason event loop will never push callback function of setTimeout() in stack.

If you remove while(1) from your code, callback for setTimeout() should get invoked.

setTimeout(() => {
  console.log('3s');
}, 3000);

4 Comments

The question wasn't about why the callback isn't called.
Please read the last line of question where OP says callback for setTimeout() is not being called and how can he ensure it gets called. If setTimeout() works, why would one need to emulate it?
He also said he put that line there to demonstrate why he thinks it's all using the same thread (which is correct). I don't know why he wants to emulate it, that's something you can ask in a comment. Either way the asked question is not answered by your answer of "make the code work by removing the code".
I think my question is not clear and make thing confuse. I add while(1) to provide that it's check timeout in the same thread, kind of provide it's a nornal function. My real question is how setTimeout work? when it check time reach? (end of function, end of block code) and how it can do that.
2

To create your own setTimeout function, you can use the following function, setMyTimeout() to do that without using setTimeout.

var foo= ()=>{
  console.log(3,"Called after 3 seconds",new Date().getTime());
}
var setMyTimeOut = (foo,timeOut)=>{
	let timer;
  let currentTime = new Date().getTime();
  let blah=()=>{

      if (new Date().getTime() >= currentTime + timeOut) {
        clearInterval(timer);
        foo()
      }
  }
  timer= setInterval(blah, 100);
}
console.log(1,new Date().getTime());
setMyTimeOut(foo,3000)
console.log(2,new Date().getTime());

4 Comments

At first, this blocks all the execution while the timeout is pending. Secondly, it is not guaranteed, that the if will be executed exactly at currentTime + timeOut , and the loop has a good change to turn to an infinite loop.
Added promise to make it async and handled infinite loop possibility
Now the syntax errors and the infinite loop are fixed, but still blocking. You simply can't emulate timeout without setting real timeout properly. A backgate would be for example to call requestAnimationFrame, but actually it is a timer as well ...
Now, non blocking, correct syntax and doesn't use setTimeout too :p but uses setInterval
2

Following is the implementation of custom setTimeout and setInterval, clearTimeout and clearInterval. I created them to use in sandbox environments where builtin setTimeout and setInterval doesn't work.

const setTimeouts = [];
export function customSetTimeout(cb, interval) {
  const now = window.performance.now();
  const index = setTimeouts.length;
  setTimeouts[index] = () => {
    cb();
  };
  setTimeouts[index].active = true;
  const handleMessage = (evt) => {
    if (evt.data === index) {
      if (window.performance.now() - now >= interval) {
        window.removeEventListener('message', handleMessage);
        if (setTimeouts[index].active) {
          setTimeouts[index]();
        }
      } else {
        window.postMessage(index, '*');
      }
    }
  };
  window.addEventListener('message', handleMessage);
  window.postMessage(index, '*');
  return index;
}

export function customClearTimeout(setTimeoutId) {
  if (setTimeouts[setTimeoutId]) {
    setTimeouts[setTimeoutId].active = false;
  }
}

const setIntervals = [];
export function customSetInterval(cb, interval) {
  const intervalId = setIntervals.length;
  setIntervals[intervalId] = function () {
    if (setIntervals[intervalId].active) {
      cb();
      customSetTimeout(setIntervals[intervalId], interval);
    }
  };
  setIntervals[intervalId].active = true;
  customSetTimeout(setIntervals[intervalId], interval);
  return intervalId;
}

export function customClearInterval(intervalId) {
  if (setIntervals[intervalId]) {
    setIntervals[intervalId].active = false;
  }
}

Comments

-1

Hi you can try this. ] HOpe it will help. Thanks

function customSetTimeOut (callback, ms) {
        var dt = new Date();
        var i = dt.getTime();
        var future = i + ms;
        while(Date.now() <= future) {
            //do nothing - blocking
        }
        return callback();
}

customSetTimeOut(function(){
  console.log("Timeout success");
},1000);

1 Comment

This doesn't emulate setTimeout because it blocks further execution.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.