Async

  • time of running code and Rendering is all tightly defined and deterministic (all thanks to event loop)
  • event loop orchestrates the threads and main thread

Browser

  1. Rendering Engine
    • Examples: Gecko, Webkit, Blink
  2. Javascript Engine (added late 1990s)
    1. Call Stack
      • Stack Frames
    2. Heap
    • Examples: Spidermonkey, JavascriptCore, V8
    • Have no knowledge of DOM, setTimeout, HTTP Request
  • Likes to repaint stuff 60 times / second -> 1 time in 16ms

Call Stack

  • one thread === one call stack == one thing at a time

Blocking Call Stack

Web APIs

  • perform setTimeout etc
  • when they are done they push item on callback queue
  • if they would have pushed directly to stack, ux would be bad.. user is doing something and in middle you would see response

Call back Queue / Task Queues

  • Receive events from Web APIs
  • oldest part of event loop

1. (Macro) Tasks

  • take one item from queue
  • if new item, push to queue
  • example: setTimeout, setInterval, setImmediate

2. Animation Callbacks - requestAnimationFrame

  • process all elements in queue
  • if new item push to new queue

3. Micro Tasks

  • process all elements in queue
  • if new item, push to same queue and process in same iteration
  • example: process.nextTick, Promise, queueMicrotask

Event Loop

First we need to understand

  1. Call Stack
  2. Web APIs (Browser) <-> C++ APIs (Node)
  3. Callback Queue
    1. Micro Task Queue
    2. Macro Task Queue
  4. Event Loop
  • Job of event loop is very simple:
if (stack.isEmpty) {
  if (!queue.isEmpty) {
    const item = queue.shift()
    stack.push(item)
  }
}
  • Shouldn't block the event loop -> don't put blocking code on stack
  • just spins round and round in a cpu efficient manner

Working:

  1. When we queue a task, event loop takes a detour
  2. Tasks are added to queue, event loop picks a task, waits for it to finish, then picks a new task

Render Steps:

  • to update what is on the screen
  1. style calculation (CSS)
  2. Layout - create render tree
  3. actual pixel data - painting

if you want to do something before browser paints - requestAnimationFrame

requestAnimationFrame(() => {
  console.log('do after 1st animation')
})

if you want to do something after browser paints - nested use of requestAnimationFrame

requestAnimationFrame(() => {
  requestAnimationFrame(() => {
    console.log('do after 1st animation')
  })
})

Promise

3 states

  1. Pending
  2. Fullfilled
  3. Rejected
new Promise(() => {}) // Pending
new Promise((res, rej) => res('Yay')) // Fullfilled
new Promise((res, rej) => rej('oh no')) // Rejected

shorthand syntax

Promise.resolve('Yay!')
Promise.reject('oh no')
console.log('start')
setTimeout(() => {
  console.log('timeout')
}, 0)
Promise.resolve('promise!').then((res) => console.log(res))
console.log('end')

// output:
// start
// end
// promise
// timeout

Async-Await

  • introduced in ES7, implicity return promise
  • When encountering an await keyword, the async function gets suspended.
  • The execution of the function body gets paused, and the rest of the async function gets run in a microtask instead of a regular task!

Difference b/w Await & Promise

const one = () => Promise.resolve('promise')
async function myFunc() {
  console.log('start of func')
  const res = await one()
  console.log(res)
  console.log('end of func')
}
console.log('before function')
myFunc()
console.log('after function')

//output:
// before function
// start of func
// after function
// promise
// end of func
const one = () => Promise.resolve('one')
function myFunc() {
  console.log('start of func')
  one().then((res) => console.log(res))
  console.log('end of func')
}
console.log('before function')
myFunc()
console.log('after function')

//output:
// before function
// start of func
// end of func
// after function
// promise

Complete Working

  1. All functions in that are currently in the call stack get executed. When they returned a value, they get popped off the stack.
  2. When the call stack is empty, all queued up microtasks are popped onto the callstack one by one, and get executed! (Microtasks themselves can also return new microtasks, effectively creating an infinite microtask loop 😬)
  3. If both the call stack and microtask queue are empty, the event loop checks if there are tasks left on the (macro)task queue. The tasks get popped onto the callstack, executed, and popped off!
const callStack = []
const microTaskQueue = []
const animationQueue = []
const taskQueue = []
while (1) {
  while (callStack.length != 0) {
    const item = callStack.pop()
    process(item) // can generate new element on stack; therefore can lead to stack overflow
  }

  while (microTaskQueue.length > 0) {
    const item = microTaskQueue.shift()
    process(item) // can generate new microtask; can create infinite loop
  }

  const temp = animationQueue
  animationQueue = []
  while (temp.length > 0) {
    const item = temp.shift()
    process(item) // new items will be added to animationQueue which is different than this
  }

  if (taskQueue.length != 0) {
    const item = taskQueue.shift()
    callStack.push(item)
  }
}