14

The following quotes are my primary references for understanding microtask queue processing:

Microtasks (which promises use) are processed when the JS stack empties.

- Jake Archibald

That doesn't make sense to me.

One go-around of the event loop will have exactly one task being processed from the macrotask queue (this queue is simply called the task queue in the WHATWG specification). After this macrotask has finished, all available microtasks will be processed, namely within the same go-around cycle.

- Stack Overflow

Now, regarding line 9 (**) in the following snippet:

From stepping through this snippet w/ debugger, the execution stack does not appear empty when these .then( callback ) microtasks are processed/executed.

Are regular functions like f2() considered a task (aka macrotask)? (when it returns it's an event loop nextTick() and the microtask queue is processed)

Why are microtasks executing when the JS stack is not empty?

function f2() {
    let x = new Promise( (resolve, reject) => { resolve( () => {console.log('howdy')} ) })
    return x
}

async function f1(){
    let y = Promise.resolve().then(() => { console.log('yo1')})
    console.log('yo2')
 let r2awaited =  await f2() //** 'yo0' and 'yo1' log when the interpreter hits this line.
 return r2awaited
}

async function start(){
     let y = Promise.resolve().then(() => { console.log('yo0')})
 let xx = await f1()
 console.log('main call return:')
 console.log(xx)
}
start()

Edit: Another peculiar finding - when you add xx() right after console.log(xx) on line 17, the stack is completely cleared prior to executing the microtasks.

The call stack 1 step prior to microtask queue processing:

The prior step

Then the immediate next step.

The next step (after microtask queue processing)

Between these two steps, the microtask queue was processed.

Does the call stack clear [under the hood] between these steps^?

And then is a new call stack created according to the required lexical environment(s) for code after the await [expression]?

Edit: At the time of posting this, I was not aware that everything below the -----(async)----- line in the chrome debugger's call stack was part of a 'fake stack'.

This 'fake stack' is presented for async debugging in a way consistent with sync debugging.

Only the elements above this -----(async)----- line are part of the real main thread call stack.

AnonEq
  • 267
  • 2
  • 9
  • Just a friendly reminder, it's good that you were clear in your question that these were quotes, but you should always _cite_ them as well, so we can see the context in which they were stated without having to search for them ourselves. You can also look at my edit so you know how quotes should be formatted for future reference. – Patrick Roberts Jul 02 '19 at 14:46
  • @PatrickRobers Agree. I should cite & format all quotes, & will do so from now on. Appreciate it. – AnonEq Jul 02 '19 at 14:49

1 Answers1

13

"Microtasks (which promises use) are processed when the JS stack empties." -Jake Archibald (doesn't make sense to me)

The "call stack" is the list of things that are currently executing:

function foo() {
  debugger;
  console.log('foo');
}

function bar() {
  foo();
  debugger;
}

bar();

When we hit the first debugger statement, the script is still executing, as is bar, as is foo. Since there's a parent-child relationship, the stack is script > bar > foo. When we hit the second debugger statement, foo has finished executing, so it's no longer on the stack. The stack is script > bar.

The microtask queue is processed until it's empty, when the stack becomes empty.

"One go-around of the event loop will have exactly one task being processed from the macrotask queue (this queue is simply called the task queue in the WHATWG specification). After this macrotask has finished, all available microtasks will be processed, namely within the same go-around cycle." - stackoverflow

Edit: I kept reading "macrotask" above as "microtask". There isn't really such a thing as a macrotask queue in the browser, it's just a task queue.

Although it's true that there's a microtask processing point after processing a task, it's only really there to handle specifications that queue tasks to queue microtasks, without calling into JS first. Most of the time, the microtask queue is emptied when the JS stack empties.

From stepping through this snippet w/ debugger, the execution stack does not appear empty when these .then( callback ) microtasks are processed/executed.

The stack will never be empty while callbacks are being executed, since the callback itself will be on the stack. However, if this is the only thing on the stack, you can assume the stack was empty before this callback was called.

Chrome's devtools tries to be helping in maintaining an "async" stack, but this isn't the real stack. The real stack is everything before the first "async" line.

Are regular functions like f2() considered a task

Being a task or a microtask isn't a property of a function. The same function can be called within a task, a microtask, and other parts of the event loop such as rendering. Eg:

function foo() {}

// Here, I'll call foo() as part of the current task:
foo();

// Here, I'll let the browser call foo() in a future task:
setTimeout(foo);

// Here, I'll let the browser call foo() in a microtask:
Promise.resolve().then(foo);

// Here, I'll let the browser call foo() as part of the render steps:
requestAnimationFrame(foo);

In your example, f2 is not called within a microtask. It's kinda like this:

function three() {}
function two() {}

async function one() {
  await two();
  three();
}

one();

Here, one() is called within the task that executed the script. one() calls two() synchronously, so it runs as part of the same task. We then await the result of calling two(). Because we await, the rest of the function runs in a microtask. three() is called, so it runs in the same microtask.

JaffaTheCake
  • 13,895
  • 4
  • 51
  • 54
  • 1
    Regarding the `This is incorrect. One go-around...`, you said it was wrong, and then proceeded to rehash more-or-less what the paragraph was saying in your own words. The quote agrees with your explanation there, if you read a little more carefully. Everything else seems okay though. – Patrick Roberts Jul 02 '19 at 14:34
  • 1
    @PatrickRoberts I think it's easy to misread 'macrotask' as 'microtask' when you're accustom to using 'task' instead of 'macrotask'. It seems this is what occurred. – AnonEq Jul 02 '19 at 14:41
  • @JaffaTheCake Awesome answer, thank you. I edited/updated my post, appending photos of the call stack at subsequent debugger steps (between which the micro task queue was processed), and two clarifying questions that I cannot find the answers to. – AnonEq Jul 02 '19 at 16:02
  • @PatrickRoberts I don't think what I said is the same. Microtasks and tasks are very different things. The quote states that one task is taken from the microtask queue per turn of the event loop. However, tasks are not in the microtask queue, they're in the task queue. Microtasks are not limited to once per turn around the event loop. – JaffaTheCake Jul 03 '19 at 16:44
  • 1
    @JaffaTheCake it says _macrotask_ not _microtask_. OP has stated that what you call "task" is what they're referring to as "macrotask". Like I said, you needed to read a little more carefully. – Patrick Roberts Jul 03 '19 at 16:56
  • @PatrickRoberts ugh, you're right. There isn't really such a thing as a macrotask in the spec. I'll update the answer. – JaffaTheCake Jul 03 '19 at 16:59