المعرفة:: JavaScript الحالة::مؤرشفة المراجع:: The Complete JavaScript Course 2022 From Zero to Expert, https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch, https://javascript.info/promise-chaining, https://javascript.info/promise-api
Synchronous vs Asynchronous
Synchronous
- Most code is synchronous.
- Simply means that the code is executed line by line, in the exact order of execution that we defined in our code.
- Each line of code always waits for the previous line to finish execution.
- This can be a problem when long-running operations block code execution.
Asynchronous
- Asynchronous literally means not occurring at the same time.
- Asynchronous code is non-blocking. It runs in the background without preventing the main code from executing.
- Execution doesn’t wait for an asynchronous task to finish its work.
- Asynchronous code is executed after a task that runs in the “background” finishes.
- Callback functions / event listeners alone do NOT make code asynchronous!
- In summary, asynchronous programming is all about coordinating the behavior of our program over a certain period of time.
- Examples: Asynchronous image loading with event and callback, Geolocation API or AJAX calls.
AJAX
- Asynchronous JavaScript And XML: Allows us to communicate with remote web servers in an asynchronous way. With AJAX calls, we can request data from web servers dynamically.
API
- Application Programming Interface: Piece of software that can be used by another piece of software, in order to allow applications to talk to each other and exchange information.
- There are countless types of APIs, e.g. DOM API - Geolocation API - Own Class API (methods available as a public interface) - “Web” API.
- “Web” API: Application running on a server, that receives requests for data, and sends data back as response.
- We can build our own web APIs (requires back-end development, e.g. with node.js) or use 3rd-party APIs.
- Most APIs these days use the JSON data format, XML is being less used.
- There are APIs for everything: weather data, countries data, flights data, currency conversion, sending email or SMS, Google Maps, etc…
XMLHttpRequest
- This is the old school way of doing AJAX in JavaScript.
- To do requests in sequence, we have to implement nested callbacks, which is known as “callback hell”.
- “callback hell” is a name for that case when we have a lot of nested callbacks in order to execute asynchronous tasks in sequence.
- The problem with callback hell is that it makes our code look very messy, harder to maintain, and very difficult to understand.
Promises
- An object that is used as a placeholder for the future result of an asynchronous operation.
- A container for an asynchronously delivered value.
- By using promises, we no longer need to rely on events and callbacks passed into asynchronous functions to handle asynchronous results.
- Instead of nesting callbacks, we can chain promises for a sequence of asynchronous operations: escaping callback hell.
- Using a promise to get a result is called “consuming a promise”. In order for a promise to exist in the first place, it must be built first.
The Promise Lifecycle
- Since promises work with asynchronous operations, they change over time and can be in different states, this is what we call the cycle of a promise.
- We are able handle these different states in our code in order to do something as a result of either a successful promise or a rejected one.
- In the very beginning, we say that a promise is pending. This is before any value resulting from the asynchronous task is available. During this time, the asynchronous task is still doing its work in the background.
- When the task finally finishes, we say that the promise is settled. There are two different types of settled promises and that’s fulfilled promises and rejected promises.
- A fulfilled promise is a promise that has successfully resulted in a value just as we expect it. To handle this fulfilled state, we can use the
then()
method and pass a callback function that we want to be executed as soon as the promise is actually fulfilled. - A rejected promise means that there has been an error during the asynchronous task.
- A promise is only settled once, then the state will remain unchanged forever.
finally()
method can be used to do something that always needs to happen no matter the result of the promise, for example, hiding a loading spinner.
- A fulfilled promise is a promise that has successfully resulted in a value just as we expect it. To handle this fulfilled state, we can use the
graph TB Pending[Pending<br>Before the future value is available] Settled[Settled<br>Asynchronoushas task has finished] Fullfilled[Fullfilled<br>Success The value is now available] Rejected[Rejected<br>An error happened] Pending -- Async Task --> Settled Settled --> Fullfilled & Rejected
Fetch API
- The Fetch API provides a JavaScript interface for accessing and manipulating parts of the HTTP pipeline, such as requests and responses. It also provides a global
fetch()
method that provides an easy, logical way to fetch resources asynchronously across the network. This kind of functionality was previously achieved usingXMLHttpRequest
. - The
Response
object, does not directly contain the actual JSON response body but is instead a representation of the entire HTTP response. So, to extract the JSON body content from theResponse
object, we use thejson()
method, which returns a second promise that resolves with the result of parsing the response body text as JSON.
Chaining Promises
- The idea is that the result is passed through the chain of
.then
handlers. - The whole thing works because every call to a
.then
returns a new promise, so that we can call the next.then
on it. - When a handler returns a value, it becomes the result of that promise, so the next
.then
is called with it.
graph TB 1[fetch] 2[.then] 3[.then] 1-->2-->3
Handling Promise Rejection
There are two ways of handling rejections:
- The first one is to pass a second callback function into the
then()
method.
- The second is to add a callback function to catch method at the end of the chain, that will handle errors globally.
Throwing Errors Manually
- In some cases, just depending on rejected promises isn’t enough to catch errors. For example, when a fetch response is 404 or any not
ok
code. - To handle this, we need to throw a new error manually, by creating a new
Error
object andthrow
it, which will immediately terminate the current function and return a rejected promise.
- Instead of handling each chained promise by having duplicate code, we can create and use a small helper function.
How Asynchronous works?
Introduction
Previously, we learnt that:
- A JavaScript Runtime is basically a container which includes all the different pieces that are necessary to execute JavaScript code.
- The heart of every JavaScript runtime is the engine, where code is actually executed and where objects are stored in memory.
- These two things happen in the call stack and in the heap.
- The Runtime also includes Web APIs environment (DOM, timers, fetch, geolocation, etc.).
- Another component of the runtime is the Callback Queue, a data structure that holds all ready to be executed callback functions that are attached to some event that has occurred.
- Whenever the call stack is empty The Event Loop takes callbacks from the callback queue and puts them into call stack so that they can be executed. So, the event loop is the essential piece that makes asynchronous behavior possible in JavaScript.
- The heart of every JavaScript runtime is the engine, where code is actually executed and where objects are stored in memory.
- JavaScript has only one thread of execution, there’s no multitasking happening in JavaScript itself, unlike other languages like Java.
Remember that one of JavaScript features is the non-blocking event loop:
9. Non-blocking event loop
A non-blocking loop allows us to take long running tasks and run them in the background, later putting it into the main thread in the future once they are finished.
وصلة للملاحظة الرئيسة
- JavaScript has a non blocking concurrency model.
- A concurrency model is simply how a language handles multiple things happening at the same time.
How Asynchronous JavaScript Works Behind the Scenes?
How can asynchronous code be executed in a non-blocking way, if there is only one thread of execution in the engine?
-
Asynchronous tasks of DOM, timers, AJAX, etc. will all run in the web API environment of the browser.
-
Example 1: Setting
.src
attribute of an image is being done asynchronously, if the image would be loading in a synchronous way, it would be doing so in the call stack blocking the execution of the rest of the code.- If we want to do something after the image has finished loading, then we need to listen to the load event. This means to register this callback in the web APIs environment, exactly where the image is loading, and the callback will stay there until the load event is emitted. (Handling asynchronous behavior here with a callback).
- Once the image has finished loading and the load event is emitted, the callback for this event is put into callback queue (an ordered list of all the callback functions that are in line to be executed).
-
Example 2: AJAX call using
fetch
will also happen in the web APIs environment. Using thethen()
method on the promise returned byfetch
will also register a callback in the web API environment so that we can react to the future resolved value of the promise.
The Event Loop
The event loop concept is very simple. There’s an endless loop, where the JavaScript engine waits for tasks, executes them and then sleeps, waiting for more tasks.
-
The Event Loop looks into the call stack and determines whether it’s empty or not, except for the global context, then if the stack is empty which means that there’s currently no code being executed it will take the first callback from the callback queue and put it on the call stack two will be executed (Event Loop Tick).
-
The event loop has the extremely important task of doing coordination between the call stack and callbacks in the callback queue.
-
The Event Loop is basically who decides exactly when each callback is executed. We can also say that the event loop does the orchestration of this entire JavaScript runtime.
Event Loop and Promises
-
With promises things work in a slightly different, callbacks related to promises doesn’t go into the callback queue, instead, they have a special queue for themselves, which is the so called micro tasks queue.
-
Micro tasks queue has priority over the callback queue.
-
At the end of an event loop tick after a callback has been taken from the callback queue, the event loop will check if there are any callbacks in the micro tasks queue, if there are any, it will run all of them before it will run any more callbacks from the regular callback queue.
-
Micro tasks queue can starve the callback queue, because if we keep adding more micro tasks, then callbacks in the callback queue can never execute. This is usually never a problem but it’s possible.
Event Loop in Practice
Building Promises
- The Promise constructor is primarily used to wrap functions that do not already support promises.
- It takes on argument, the executor function, which takes two arguments, resolve and reject functions.
- To create a new Promise:
const promise1 = new Promise(function(resolve, reject) {function body}
. - In
resolve()
function, we pass the fulfilled value of the promise that will be later consumed usingthen()
method. - In
reject()
function, we pass the error message that will be used later usingcatch()
method. - To consume it:
promise1.then((value) => { console.log(value) });
Example:
Promisifying
- Usually we build promises to wrap old callback based functions into promises, this is a process that we call Promisifying.
- Promisifying means to convert callback based asynchronous behavior to promise based.
- To do this, usually we create a function, then inside of this function we create and return the promise.
Example 1
- In the previous example, having a nice sequence of asynchronous behavior is way better than callback hell example.
Transclude of ,-JavaScript-Asynchronous#^callbackhellexample
Example 2
Remember that navigator.geolocation.getCurrentPosition()
,-JavaScript-Geolocation-API#^getcurrentpositionparametersSo we don’t need to pass
resolve
and reject
manually.
Example 3
Creating an immediately fulfilled or a rejected promise
- To create an immediately fulfilled promise we can use
Promise.resolve()
static method of the promise constructor and pass in the resolved value. - We can also do the same with
Promise.reject()
.
Consuming Promises with async
and await
- Since ES2017, there is now a better and easier way to consume promises, which is called
async
await
. - An
async
function is a special kind of function that will basically keeps running in the background while performing the code that inside of it, then when it is done, it will automatically return a promise. - Inside an
async
function, we can have one or moreawait
statements.await
will stop the code execution at this point of the function until the premise is fulfilled. - Stopping the code inside an
async
function isn’t a problem because the function is running asynchronously in the background and therefore it is not blocking the main thread of execution. async
andawait
makes our code look like regular synchronous code while behind the scenes everything is in fact asynchronous, that makes it so much easier and more clean. We now are able to store the fulfilled promise value immediately into a variable without having to mess with callback functions.
Full example:
Error Handling with async
and await
- With
async
/await
, we can’t use thecatch()
method that we use before, but we can use atry
catch
statement.
Returning Values from async Functions
async
functions return aPromise
.- The returned promise can be consumed normally using
then()
method. - In case of errors happening inside
try
block, the promise won’t be rejected. To fix this the error caught incatch
block must be re-thrownthrow err;
. finally()
will be executed always.
- IIFE’s can be utilized to use
await
and receive data from other async functions instead of having to reply on Promise’sthen()
,catch()
andfinally()
.
Promise Combinator Functions
Running Promises in Parallel with Promise.all()
Promise.all(promises)
takes an iterable of promises, waits for all promises to resolve and returns an array of their results. If any of the given promises is rejected, it becomes the error ofPromise.all
, and all other results are ignored.
Promise.race()
Promise.race(promises)
waits for the first promise to settle, and its result/error becomes the outcome promise. It’s similar toPromise.all
, but waits only for the first settled promise and gets its result (or error).
- In the real world
Promise.race()
is very useful to prevent against never ending promises or very long running promises. For example, if the user has a very bad internet connection, then a fetch requests in your application might take way too long to actually be useful, so we can create a special time out promise, which automatically rejects after a certain time has passed.
Promise.allSettled()
- Introduced in ES2020,
Promise.allSettled(promises)
waits for all promises to settle and returns their results as an array of objects with:status
: “fulfilled” or “rejected” andvalue
(if fulfilled){status:"fulfilled", value:result}
or reason (if rejected){status:"rejected", reason:error}
. - It’s good for “all or nothing” cases, when we need all results successful to proceed.
Promise.any()
Promise.any(promises)
, added in ES2021, waits for the first promise to fulfill, and its result becomes the outcome. If all of the given promises are rejected,AggregateError
becomes the error ofPromise.any
.
Top-Level await
- Starting from ES2022 version, we can now use the
await
keyword outside ofasync
functions, at least in modules (<script type="module">
).
- While this is all great and very useful, this actually blocks the execution of the entire module now.
- If one module imports a module which has a top-level
await
, then the importing module will wait for the imported module to finish the blocking code.