0

I was asked during the interview

"Is it possible to have a piece of code to be scheduled to execute immediately - i.e. jumping the queue - without putting it at the end of event loop's queue".

Initially I thought that this is something done by requestIdleCallback (as I recently read about React Fiber), but after checking requestIdleCallback docs I'm not sure if this is how it actually works i.e. when current callstack gets empty and there was requestIdleCallback called, then ignore what is in engine's event queue and run the callback.

So... Is it possible or not?

dragonfly
  • 17,407
  • 30
  • 110
  • 219
  • "scheduled to execute immediately" ? maybe the interviewer was expecting a synchronous call ? Or maybe he was expecting `setTimeout(somefunction)` which isn't really immediate but almost. As it is, this question looks a little too open and vague for SO. – Denys Séguret Apr 17 '19 at 13:32
  • the `process` seems to apply to node.js - however, my question refers specifically to the browser environment. – dragonfly Apr 17 '19 at 13:40
  • `runs immediately after scheduling`, What do you mean by this?, IOW: what scheduling are we talking about here. If you did `let x = y + 1`, this will execute `now`, the event loop doesn't get a look in. – Keith Apr 17 '19 at 13:46
  • This bit might be what your after -> `however, callbacks which have a timeout specified may be called out-of-order` So I suppose if you have a small timeout, the answer is yes.. – Keith Apr 17 '19 at 13:50
  • by out of order I mean: imagine that `setTimeout(fn, 0)` is implemented in a way that adds `fn` to the beginning of the event loop or a mechanism that works like scheduling a microtask (e.g. `Promise` does that): https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/ – dragonfly Apr 17 '19 at 13:54

3 Answers3

1

Yes it's possible, this is even exactly what the queue a microtask task is supposed to do: push a new microtask at the end of the current job queue, i.e before the end of the current event loop.

This can be achieved by the quite new Window.queueMicrotask() method (which is currently only supported by Webkit & Blink if I'm not mistaken).

if(!window.queueMicrotask)
  console.error("your browser doesn't support the queueMicrotask method");
else {
  // idle
  if(window.requestIdleCallback)
    requestIdleCallback(() => console.log('five idle'));
  // timeout
  setTimeout(() => console.log('four timeout'), 0);
  // message event (beginning of next event loop)
  onmessage = e => console.log('three message');
  postMessage('', '*');

  // "queue a microtask"
  queueMicrotask(() => console.log('two microtask'));
  // synchronous
  console.log('one sync');
}

But even in browsers that don't support this method, we have since quite long time access to other methods that will "queue a microtask".

For one, Promise.resolve's callback will get queued as a microtask, so we could also just do

Promise.resolve()
  .then(functionToBeCalledBeforeEventLoopsEnd);

// idle
if(window.requestIdleCallback)
  requestIdleCallback(() => console.log('six idle'));
// timeout
setTimeout(() => console.log('five timeout'), 0);
// message event (beginning of next event loop)
onmessage = e => console.log('four message');
postMessage('', '*');

// Promise microtask
Promise.resolve().then(() => console.log('two Promise.resolve microtask'));

// obvious microtask (third because Promise's as been queued before)
window.queueMicrotask && queueMicrotask(() => console.log('three microtask'));
// synchronous code
console.log('one sync');

But even before Promises, it was already possible to queue a microtask, by using the MutationObserver API, since mutation records have to be queued as microtasks.

// idle
if(window.requestIdleCallback)
  requestIdleCallback(() => console.log('six idle'));
// timeout
setTimeout(() => console.log('five timeout'), 0);
// message event (beginning of next event loop)
onmessage = e => console.log('four message');
postMessage('', '*');

// Mutation microtask
const obs = new MutationObserver(() => console.log('two mutation-microtask'));
obs.observe(test, { attributes: true });
test.className = 'foo';

// obvious microtask (third because Mutation's has been queued before)
window.queueMicrotask && queueMicrotask(() => console.log('three microtask'));
// synchronous code
console.log('one sync');
<div id="test"></div>

But beware, this also means that you can create infinite loops even if these methods are asynchronous, because the event loop would never reach its end.

const asynchronousBlockingLoop = () =>
  queueMicrotask(asynchronousBlockingLoop);

And for what requestIdleCallback does, it is waiting for when the browser has nothing to do anymore, and this can be in quite a few event loops.

Kaiido
  • 123,334
  • 13
  • 219
  • 285
0

I'd rather say "No", because of the way the JS runtime works and you cannot alter that from within JS code.

The runtime processes all events and tasks in a queue sorted by age (oldest actions/tasks/etc. are processed first) and the runtime "runs-to-completion" (meaning it processes each task from start to end before running the next task).

For example, if you write setTimeout( <FUNCTION> , 0), the function won't be executed immediately – instead, the browser pushes a message to the queue and processes it as fast as possible. Even if there isn't anything else the runtime does at that point, the function is executed with a delay of a few milliseconds (note that the number passed in isn't guaranteed to be the exact time when your function runs – it instead resembles a guaranteed amount of time which passes by before the function runs). The same goes for setImmediate (where supported).

MDN

Update The above isn't exactly true for Nodejs. In Node, using process.nextTick, you can place an action at the front of the queue. However, it won't be processed immediatly if the event loop already processes another task. Therefor, the correct answer should be "Yes, but only if your code runs on Nodejs, it is possible to place a task at the beginning of the queue. But it still isn't guaranteed to run immediatly".

David
  • 3,552
  • 1
  • 13
  • 24
0

Since the event loop only processes timeouts that are in the queue when it starts, this should work on both Node and the browser:

function skipTicks(n,f) {
  if(n<=0) setTimeout(f)
  else setTimeout(() => skipTicks(--n,f))
}
AnyWhichWay
  • 716
  • 8
  • 11