NOTE for: Tasks, microtasks, queues and schedules
This is the note for This Article And Thanks for Author
First let’s take a look with the code:
console.log('script start') |
and result:
script start |
Microsoft Edge, Firefox 40, iOS Safari and desktop Safari 8.0.8 log setTimeout
before promise1
and promise2
- although it appears to be a race condition. This is weird, as firefox 39 and Safari 8.0.7 get it consistently right.
Why this happens
This is something about event loop handles tasks and microtasks
.
Each thread
gets its own event loop, so such web worker gets its own, so it can execute independently communicate. The event loop runs continually, evecuting any tasks queued. An event loop has multiple task sources which guaratees evecution order within that source, but the browser gets to pick which source to take a task from on each turn of the loop. This allows the browser to give preference to performance sensitive task such as user-input.
Tasks: scheduled so the browser can get from its internals into JavaScript/DOM land and ensures these actions happen sequentially. Between tasks, the browser may render updates. Getting from a mouse click to an event callback requires scheduling a task, as does parsing HTML, and setTimeout
setTimeout
waits for a given delay then schedules a new task for its callback. This is why setTimeout
is logged after script end
, as logging script end
is part of the first task, and setTimeout
is logged in a separate task.
Microtasks: usually scheduled for things that should happen straight after the currently executing script, such as reacting to a batch of actions, or to make something async without taking the penalty of a whole new task. The microtask queue is processed after callbacks as long as no other JavaScript is mid-execution, and at the end of each task. Any additional microtasks queued during microtasks are added to the end of the queue and also processed. Include mutation abserver callbacks, and promise callbacks.
Promise settles(settled) -> queues a microtask for its reactionary callbacks. (ensures promise callbacks are async even if the promise has already settled).
So calling .then()
against a settled promise immediately queues a microtask. This is why promise1
and promise2
are logged after script end
, as the currently running script must finish before microtasks are handled. promise1
and promise2
are logged before setTiemout
, as microtasks always happen before the next task.
Summary
- Tasks: Scheduled so the browser can get from its internals into JavaScript JavaScript/DOM land and ensures theses actions happen sequentially. Between tasks, browser may render updates, Geting from a mouse to an event callback requires scheduling a task, as does parsing HTML, and setTimeout
- Microtasks: Scheduled for things taht should happen straight after the currently executing script.
- JS stack: JS execute stack.
What are some browsers doing differently?
Some browsers log script start
, script end
, setTimeout
, promise1
, promise2
. They’re running promise callbacks after setTimeout
. It’s likely that they calling promise callbacks as part of a new task rather than as a microtask.
It’s work because promises come from ECMAScript rather tan HTML. ECMAScript has the concept of “jobs” which are similar to microtasks, but the relationship isn’t explicit aside from vague mailing list discusions.
Treating promises as task leads to performance problems, as callbacks may be unnecessarily delayed by task-related things such as rendering. It also causes non-determinism due to interaction with other task sources, and can break interactions with other APIs.
How to tell if something uses tasks or microtasks.
Testing see when logs appear relative to promises & setTimeout, although you’re relying on the implementation to be correct.
The certain way, is to loop up the spec:
In ECMAScript land, they call microtasks jobs
. In step 8.a of PerformPromiseThen, EnqueueJob is called to queue a microtask.
Level 1 bossfight
<div class="outer"> |
Given the following JS, what will be logged if I click div.innter
?
// Let's get hold of those elements |
Result is different in different browser
Who’s right?
Dispatching the click
event is a task. Mutation observer and promise callbacks are queued as microtasks. The setTimeout
callback is queued as a task.
First task:
Dispatch click
task- Log
click
- Add task
setTimeout callback
- Then
Promise then
will be declare in Microtasks. Mutation queues
a microtask to handle observersonClick
event finish in JS stack
It’s time to execute microtasks
- First
Promise then callback
will be execute - Log
promise
- Then
Mutation callback
execute - Log
mutate
Because event bubbles, so our callback is called again for the outer element
- Log
click
- Add task
setTimeout callback
- Add microtask
Promise then
- Add microtask
Mutation callback
- Microtask
Promise then
will be execute - Log
promise
- Microtask
Mutation observers
will be execute - Log
mutate
- Then tasks
Dispatch click
clear, Move tosetTimeout callback
task - Log
setTimeout
- Move to next
setTimeout callback
task - Log
setTimeout
This is the right way in Chrome. That microtasks are processed after callbacks (as long as no other JavaScript is mid-execution), It may was limited to end-of-task. This rule:
If the [stack of script settings objects](https://html.spec.whatwg.org/multipage/webappapis.html#stack-of-script-settings-objects) is now empty, [perform a microtask checkpoint](https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint)
- [HTML: CLeaning up after a callback](https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-a-callback) step 3
A microtask checkpoint involves going through the microtask queue, unless we;re already processing the microtask queue. Similarly, ECMAScript says this of jobs:
Execution of a Job can be initiated only when there is no running execution context and the execution context stack is empty...
- ECMAScript: Jobs and Job Queues
What did browsers get wrong?
Firefox and Safari are correctly exhausting the microtask queue between click listeners. as shown by the mutation callbacks, but promises appear to be queued differently. This is sort-of excusable given that the link between jobs & microtasks is vague, but I’d still expect them to execute between listener callbacks.
With Edge we’ve already seen it queue promises incorrectly, but it also fails to exhaust the microtask queue between click listeners, instead it does so after calling all listeners, which accounts for the single mutate
lig after both click
logs.
Level1 boss’s angry older brother
Same example from above, If we execute:
inner.click() |
Why is it different?
- Run script
- inner.click() will be executed and JS stack boot into
onClick
- Log
click
setTimeout
task been declared- Microtasks
Promise then
has been created - And add Microtasks
Mutation abservers
After this, we cannot process microtasks, because JS stack is not empty
- Log
click
setTimeout
task been declared- Microtasks
Promise then
has been created
And at this time, we cannot add another mutation microtask as one is already pending.
Script
is gone, we process microtasks
- Microtasks
Promise then
executed and logPromise
- Microtasks
Mutation observers
executed and logMutate
- Microtasks
Promise then
executed and logPromise
Run script task is finished, move to execute two setTimeout callback
task
- Task
setTimeout callback
and logtimeout
- Task
setTimeout callback
and logtimeout
Previously, this meant that microtasks run between listener callbacks, but .click()
causes the eent to dispatch synchronously, so the script that calls .click()
is still in the stack between callbacks. The above rule ensures microtasks don’t interrupt JavaScript that’s mid-execution.
This means we don’t process the microtask queue between listener callbacks, they’re processed after both listeners.
Summary
- Task execute in order, and the browser may render between them
- Microtasks execute in order, and are executed:
- After every callback, as long as no other JavaScript is mid-execute
- At the end of each task
Other Funny Thing
- Great talk at JSConf on the event loop
- IndexedDB
- Living Standard
- ECMAScript® 2015 Language Specification