Lets say the call stack has 5 things on it and one item in the event queue. Once all 5 items get popped off the call stack, the callback from the event queue gets pushed onto the callstack (which may take 20 seconds to complete). In the meantime, I have added another (non-blocking) call to the call stack. How does this work if the I/O intensive operation is still executing? Does the system temporarily freeze?
-
1What do you mean by "I have added another (non-blocking) call to the call stack."? Code is worth 1024 words.T.J. Crowder– T.J. Crowder2019-01-26 08:57:21 +00:00Commented Jan 26, 2019 at 8:57
-
Let's say you perform some operation, like clicking a button, that adds another call onto the call stack. A button may be a bad example since an event listener is attached to it, which would, in turn, get added into the callback queue.John Lippson– John Lippson2019-01-26 08:58:50 +00:00Commented Jan 26, 2019 at 8:58
-
1That adds a job to the job queue, rather than a call to the call stack. I've updated my answer. :-)T.J. Crowder– T.J. Crowder2019-01-26 09:02:54 +00:00Commented Jan 26, 2019 at 9:02
1 Answer
As you said, it's a loop, or as the JavaScript spec puts it, a job queue. The initial execution of the top-level of the script is a job; an event handler callback is a job; a timer callback is a job; etc.
When a job is picked up from the job queue, it is run to completion.¹ If that takes 20 seconds, it takes 20 seconds. The thread processing that job queue can't do anything else during those 20 seconds. If you do this on the main UI thread in a web browser, it largely freezes the browser's UI. (If you do it on a worker thread, of course, it just blocks the worker thread.)
I asked what you meant by adding a (non-blocking) call to the call stack. You said:
Let's say you perform some operation, like clicking a button, that adds another call onto the call stack.
Clicking a button that has an event handler doesn't add a call to the call stack; it adds a job to the job queue. (A function call, foo();, in the executing code of a job adds a call to the call stack.) If the thread is busy processing another job, that job sits there waiting to be picked up when the thread is done with whatever job it's currently working on.
I should note that there are two standard kinds of jobs: Script jobs and promise jobs. (Or as the HTML spec calls them, tasks and microtasks.) The main script execution, DOM event callbacks, and timer callbacks are all script jobs / tasks (aka "macrotasks"). Promise reactions (calling the promise's fulfillment or rejection handlers) are promise jobs / microtasks. The difference is that when a script job (task) is running, any promise jobs (microtasks) it schedules will get run when that job ends, instead of being added to the main job queue. Any promise jobs scheduled by a promise job get run during that same end-of-script-job processing. That is, promise jobs / microtasks have higher priority than script jobs / tasks.
You can see that happening here:
// This script is running in a script job / task
// Unsurprisingly, this is the first thing you see in the console
console.log("Main script job begin");
// Here, we schedule a script job / task for an immediate timer callback:
setTimeout(() => {
console.log("Timer job");
}, 0);
// After doing that, we schedule a Promise fulfillment callback:
Promise.resolve().then(() => {
console.log("Promise fulfillment job 1 begin");
Promise.resolve().then(() => {
console.log("Promise fulfillment job 2");
});
console.log("Promise fulfillment job 1 end");
});
// For emphasis, we'll output something before either happens;
// this is the second thing you see in the console.
console.log("Main script job end");
The output is:
Main script job begin Main script job end Promise fulfillment job 1 begin Promise fulfillment job 1 end Promise fulfillment job 2 Timer job
When the browser loads the script, it queues a job in the script jobs queue to run that script. The JavaScript thread picks up that job the next time it does its loop, and this happens:
- The first message is output saying the main job began.
setTimeoutis called, scheduling a timer callback in ~0ms. That adds the timer to the host's list of timers with a scheduled execution time ~0ms in the future. Since the delay is 0ms, the host may or may not immediately add a script job to the script job queue to call the timer callback (or it may wait to do that until a tiny bit later).Promise.resolveis called, creating a promise fulfilled with the valueundefined.thenis called on that settled promise. Since the promise is settled, that immediately adds a job to call the fulfillment callback to the promise jobs queue for the current script job.- The second message at the end of the script is output, showing that the main script job is ending.
- Since the script job has reached the end, its promise jobs queue is processed. There's an entry in it (to call our first fulfillment handler), so that function gets called.
- That function:
- Outputs its "Promise fulfillment job 1 begin" message.
- Creates another fulfilled promise.
- Calls
thento add a fulfillment handler to it.- Since the promise is already settled, that immediately adds a job to the promise jobs queue to call the second fulfillment handler.
- Outputs its "Promise fulfillment job 1 end" message.
- Since that promise job completed, the JavaScript engine looks at the promise jobs queue to see if there are any remaining entries. There is one, so it calls the function that entry specifies.
- That function outputs its "Promise fulfillment job 2" message.
- Since that promise job completed, the JavaScript engine looks at the promise jobs queue to see if there are any remaining entries. There aren't, so the script jobs loop continues.
- The job to call the timer callback may be in the script jobs queue at this point. If it isn't, the host environment may add it at this point or may let the event loop cycle a couple of times. Eventually, though, it definitely puts the job in the script jobs queue to call the timer callback.
- The JavaScript engine picks up that job, calls the timer callback, and the callback outputs its message.
So even though it's possible that the timer callback was added to the script jobs queue in Step 2 before the first promise fulfillment handler was put in the promise jobs queue in Step 4, that fulfillment handler is run first. And because that promise job queued another promise job (Step 7.3.1), that second promise job also gets run first.
I say two "standard" kinds of jobs/tasks because environments provide other things (like Node.js's setImmediate or a browser's requestAnimationFrame) which are somewhat separate from the two main job types / queue types.
¹ The thread can be paused, via Atomics.wait, but it can't be used to process another job from the queue. Most JavaScript engines don't allow pausing the main thread (the UI thread in browsers, the main thread in Node.js), but do allow pausing worker threads.