- concurrent asynchrony
- async operations are created using Promises or Web APIs
- beware: JS operations are not only synchronous (without Web APIs), e.g. Promises are asynchronous ❗️
- beware: Web API operations aren’t all asynchronous, e.g. BOM API and DOM API are synchronous ❗️
Operations
- top-level of script is like a main operation, runs first, schedules any nested async operations
- after main operation is done, async operations run, can schedule themselves more nested async operations
- order of async operations depend on type:
- first all microtasks, e.g. Promises, MutationObserver API, etc.
- then one macrotask, e.g. Timeout API, Fetch API, Event API, etc.
- beware: order of nested async operations gets confusing quickly ❗️
// running order: op_main, op2, op21, op1, op211, op11, op111
//op_main
setTimeout(() => {
//op_1: macrotask
setTimeout(() => {
//op_11: macrotask
Promise.resolve().then(() => {
//op_111: microtask
console.log("op_111");
})
console.log("op_11");
}, 0)
console.log("op_1");
}, 0)
Promise.resolve().then(() => {
//op_2: microtask
Promise.resolve().then(() => {
//op_21: microtask
setTimeout(() => {
//op_211: macrotask
console.log("op_211");
}, 0)
console.log("op_21");
})
console.log("op_2");
})
console.log("op_main");
Code
calls asynchronous function passes handler function that runs with results of async operation
see CPS, Promises, Async await
needs to write “parallel” code in a linear text file, like multiple dimensions in single dimension
beware: order of code isn’t order of execution anymore, can’t read code top-to-bottom to know what is run when ⚠️
beware: operations are separate, can’t merge code paths again after separated, async code is called only after sync code is done ⚠
beware: same handler may be called multiple times, e.g. Event API ❗️
Implementation
RE runs JS on a single thread, “main thread”
Web browser uses same main thread to create DOM, e.g. parse HTML, style calc, layout, paint, etc.
many Web APIs run on separate thread, don’t block main thread where JS + DOM runs some don’t need separate thread, e.g. Timeout API just puts into queue, only needs threads for actual tasks, e.g. network, storage, etc.
beware: don’t confuse Web APIs with handlers, all JS runs on main thread, only actual work of Web API is on separate thread, e.g. fetch, I/O, Web Workers, etc.
depends on implementation if threads in same or different processes, e.g. different processes in Chrome
Event Loop
- loop in RE that selects operation(s) for execution
- one iteration is called a “tick”
- like a manager that decides what operation is run when
- if no operation runs, event loop takes a handler from queue and executes it
- beware: only one handler can run at a time ❗️
- beware: handler is always run to completion uninterrupted, e.g. no flashing screen while changing the DOM ❗️
- Web browser also created DOM at the end of a tick if necessary
- beware: a long running handler blocks the thread, because the event loop can’t continue, e.g. in browser UI freezes ⚠️
- split long running handler into smaller async operations, if purely computational use seperate thread through Web Workers
Task Queues
scheduling an async operation, puts the handler into a queue ?? if Web API then only after RE did some work, often in separate thread gets results from other threads back onto main thread
can think of as mailbox that collects mail to open later
multiple queues, depend on runtime with Web APIs, differ in when and how they are run for a tick, from some queues only a single handler is run, from some queues all handlers are run (no new ones), from some all handlers are run (even new ones)
order of queues for one tick
- all existing and new microtasks, e.g. Promises, MutationObserver API, etc.
- one macrotask, e.g. Timeout API, Fetch API, Event API, etc.
- all existing requestAnimationFrame if rendering
- the render pipeline if needed, adapts intelligently to device, e.g. every ~16 ms on 60 FPS screen, not if in background, etc.
- own queues:
IndexedDB
,requestAnimationFrame
, etc.
beware: implementation can still vary, e.g. skip Timeout API macrotask for rendering mouse clicks, etc. ❗️
microtask run even mid macrotask if no operation is running anymore, e.g. between two listener for single
beware: event initiated through JS can behave differently than through user, because through JS is scheduled synchronously, and microtasks can’t run mid macrotask anymore because the main operation is still running ⚠️
beware: a long queue delays execution of handlers, because needs to wait until all handlers before are run, e.g.
setTimeout(() => {}, x)
may not be in x milliseconds ⚠️beware: an infinite Microtask queue blocks the thread, because the event loop can’t continue ⚠️
?? how does Web API on macrotask queue can return a promise on the microtask queue ??
- promisifying non-promise Web API goes through macrotask queue and then through microtask queue during same tick, but shouldn’t be a problem since microtask queue is always empty before a macrotask runs
function timeout(delay, args) {
return new Promise((res, _) => {
setTimeout(res, delay, args)
})
}
timeout(1000).then(() => console.log("Hello World!"))
queueMicrotask()
- schedules a microtask
- executed in same tick
Non-determinism
- order of async operations from Web APIs is non-deterministic can’t guarantee order of asynchronous operations from Web APIs
- since handler is put into queue only after Web API came back, and Web API may depend on external factors, e.g. IO, network, etc.
- beware: even for simple Web APIs shouldn’t rely on order, e.g. two subsequent
setTimeout(..., 0)
calls may or may not be in order ❗️
// logs a b or b a, no guarantees
fetch("a.com").then(() => {console.log("a")});
fetch("b.com").then(() => {console.log("b")});
Race conditions
- non-deterministic async operations that access and modify the same shared state, e.g. global variables, DOM, etc.
- result changes for different executions
- since handlers share same state because in same scope
- common source of bugs
// logs 6 or 4, no guarantees
let a = 1;
Promise.all([
fetch("a.com").then(() => {a = a + 2;}),
fetch("b.com").then(() => {a = a * 2;})
]).then(() => {console.log(a)});