Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does this line of code with 'await' trigger microtask queue processing?

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.

like image 382
AnonEq Avatar asked Jul 02 '19 11:07

AnonEq


Video Answer


1 Answers

"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.

like image 130
JaffaTheCake Avatar answered Oct 26 '22 16:10

JaffaTheCake