Search code examples
javascriptgoogle-chromev8

Which part of the Javascript runtime is responsible for putting the callback into the callback queue?


Consider this code running on the chrome console:

function main(){
  setTimeout(()=>console.log('Hello World!'), 5000);
};

main();

As per my understanding:

  1. The V8 engine will push main() into the call stack.
  2. The setTimeout() web API binding will be called by the engine, which in turn will trigger some native code outside of the main javascript thread.
  3. main() will be popped off the stack
  4. Once the 5 seconds has elapsed, the event loop will retrieve the callback from the callback queue and add it onto the call stack for execution.

My question (which I think is a very minute detail, but has been bugging me for awhile) at which point and by whom is the callback pushed onto the callback queue?


Solution

  • You can think of the event loop as this piece of pseudo-code at the core of the JavaScript engine:

    while (true) {
      while (queue.length === 0) {
        sleep();  // Native way to let the current process sleep.
      }
      callback = queue.pop_first();
      callback();
    }
    

    And then the engine exposes a public function to its embedder:

    function ScheduleCallback(callback) {
      queue.push_last(callback);
    }
    

    I'm obviously glossing over a bunch of details here (synchronization of queue access, waking up when something gets queued, graceful termination, prioritization of callbacks, ...), but that's the general gist of it.

    To implement setTimeout, an embedder would use some other primitive that provides a waiting function (e.g. it could spin up a thread and put that thread to sleep for the desired time, or it could rely on a function similar to setTimeout provided by the operating system, i.e. some way to trigger a callback after a specified amount of time), and then call the function mentioned above. In pseudo-code:

    engine.global.setTimeout = function(callback, delay) {
      OS.kernel.setTimeout(delay, () => {
        engine.ScheduleCallback(callback);
      });
    }
    

    This also explains why timeouts are not guaranteed to be precise: firstly, the operating system's timer primitive might have its own granularity constraints and/or might simply be busy; secondly the best an embedder can do is to schedule the callback after the specified time, but if your event loop is currently busy executing something else, then the scheduled callback will have to wait in the queue for its turn.

    Side note: "pushing a function onto the call stack" is exactly the same as calling it; "popping it off the call stack" is exactly the same as having it return. The line "callback();" in the first snippet above does both. It's that simple!