Asynchronous JS stuff

Pere Sola - Jan 7 - - Dev Community

Notes from The hard parts of asynchronous Javascript course in Frontendmasters, by Will Sentance. The links above are related to the topic.

  • When the code runs: global execution context. 1) Thread of execution (parsing and execution line by line) and 2) Global Variable Environment (memory of variables with data).
  • When a function gets executed, we create a new execution context. 1) thread of execution (go through the code in the function line by line) 2) local memory (variable environment) where everything in the function is stored (garbage-collected when the execution context is over).
  • Keeping track of the execution context needs the call stack. The global execution context will always be at the bottom when we are running the code.
  • Synchronicity and single threaded are two pillars of JS. How to make it asynchronous not to block the single thread?
  • Web APIs are not part of JS (see here), but allow JS to do more stuff. Web APIs are in the web browser.
  • Stuff outside JS is allowed back in (i.e. setTimout(printHello, 1000)) when 1) I have finished running all my code (the call stack is empty) and 2) the call stack is empty. The stuff "outside" (the browser feature) gets added the the callback queue (called task queue in the specs) before it's allowed back in. Only when 1) and 2) are done, JS looks inside the callback queue and adds it in the call stack. That's called the event loop. And only when the function printHello is added to the call stack the code inside will run (printHello doesn't run "inside" setTimeout).
  • Promises interacts with this world outside JS but it immediately returns an object (a Promise object), which acts as a placeholder of the data that will come later. The data will eventually be in the value property. Promises have an onFulfilled hidden property that runs when the data is available (array of functions that run when value gets updated). "Two pronged" solution: JS 1) returns the Promise with a placeholder and 2) initiates the browser work (xhr request). We don't know when value will be filled with data, so we add functions inside the onFulfilled that will run once we have the data, with value as input to these functions. .then adds a function to the onFulfilled array. When these functions run, they are added to the microtask queue (job queue in the specs). The event loop prioritises tasks in the microtask queue over the callback queue (the microtask queue should be empty before the callback queue is allowed in the call stack. The promise has another properties, status with 3 possible values: pending, fulfilled and rejected. The rejected status will trigger a similar array to onFulfilled, but called onRejected. We add function there with the .catch (or the 2nd argument to .then (see mdn here).
  • Closure === lexical scope reference. Iterator is a function that when called and executes something in an element it gives me the next element in the data. Iterators turn our data into "streams" of data, actual values that we can access one after another. The "closed over" data is stored in a hidden property called [[scope]].
  • Generators. Defined with a * in the function declaration (i.e. function* createFlow() {}). When we call createFlow (createFlow()), it returns a Generator object (mdn docs here) with a next method (here), that can be called afterwards. Like the code below. Generator functions do not have arrow function counterparts.
function *createFlow() {
    yield 4;
    yield 5;
}

const returnNextElement = createFlow()
const elementOne = createFlow.next()
const elementTwo = createFlow.next()
Enter fullscreen mode Exit fullscreen mode
  • Calling the .next of a generator object creates an execution context, with a local memory. When yield is reached, the execution of this context is "paused". The returned value of next is:
{
    value: 4,
    done: false, // or true if we are yielding the last value
}
Enter fullscreen mode Exit fullscreen mode
  • When we pass values to next the first yield will not return any value, because of the power of the yield keyword which, like return kicks us out of the execution context. Next time we call next with a value, the previous yield will evaluate to that value, and the execution resumes. See example in the course (~17.25 in Generator Functions with Dynamic Data lesson) and in mdn.
  • The "backpack" (closure) of the generator has not also the line where execution "stopped", which allows us to continue execution when next is called.
  • In asynchronous JS (i.e. fetch):
function doWhenDataReceived(value) {
    returnNextElement.next(value);
}

function* createFlow() {
    const data = yield fetch(url);
    console.log(data);
}

const returnNextElement = createFlow();
const futureData = returnNextElement.next();

futureData.then(doWhenDataReceived)
Enter fullscreen mode Exit fullscreen mode
  • ^ 1) Create const returnNextElement to which the return value of createFlow gets assigned (`generator object); 2) create const futureData 3) we open createFlow execution context and we reach the yield keywords, which "pauses" the execution of the function and allows to assign to futureData the promise object created by calling the fetch API; 3) .then adds that function to the onFullfilled array in the promise object (it will get triggered when the value of the promise is updated); 4) the fetch api returns the data, and the function inside onFullfulled gets added into the microtask queue (job queue officially) and, since the call stack is empty + no more code to run, it gets added into the call stack; 5) doWhenDataReceived execution context starts, it calls the .next from the generator object, which brings us back to createFlow where the execution was paused; 6) data gets assigned the value of the promise, and then we resume execution of the rest of the code inside that execution context.
  • async / await simplifies all this. The resumption of createFlow is done automatically. But this still gets added to the microtask queue:

`
async function createFlow() {
console.log("Me first");
const data = await fetch(url);
console.log(data);
}

createFlow();

console.log("Me second");
`

  • await is similar to yield, it throws you out of the execution context. 1) define createFlow; 2) call createFlow and open an execution context; 3) console log "me first"; 4) define data variable - we don't know what it will evaluate to but the fetch creates a promise object, which triggers a browser xhr request - the promise object has the value property and the onFullfilled; 5) await, like yield, kicks us out of the execution context and pauses it; 6) console log "me second" runs; 7) when the data is back, the execution of createFlow resumes by assigning the variable data to the value of the promise; 8) console log data.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .