Execution Process and Order of Asynchronous Code
I referenced the link: https://helloworldjavascript.net/pages/285-async.html.
Asynchronous Programming
Timer API
setTimeout(() => { console.log("2000 ms"); }, 2000); setInterval(() => { console.log("3000 ms"); }, 3000);
These are built-in functions that allow you to execute a function after a specific amount of time or repeatedly at specified intervals.
Both
setTimeout
and setInterval
return a timer identifier, which you can use to cancel the running timer.const timeoutId = setTimeout(() => { console.log('2 seconds have passed since setTimeout was triggered.'); }, 2000); const intervalId = setInterval(() => { console.log('This will be printed every 3 seconds.'); }, 3000); clearTimeout(timeoutId); clearInterval(intervalId); // Nothing will be printed.
Things to watch out for with timers:
setTimeout
and setInterval
do not guarantee exact delay times.const start = new Date(); setTimeout(() => { console.log(new Date() - start); }, 100); // There's a slight difference between the actual and expected delay time.
Also, when you give a delay time of 0, the code may not behave as expected:
setTimeout(() => { console.log('hello'); }, 0); console.log('world'); // Output: // world // hello
Browser JavaScript Execution Process
Call Stack
The call stack is a stack-shaped storage that manages information related to function calls in the JavaScript engine.
For the following code, the call stack looks like this:
function add(x, y) { return x + y; } function add2(x) { return add(x, 2); // Calls `add` } function add2AndPrint(x) { const result = add2(x); // Calls `add2` console.log(result); // Calls `console.log` } add2AndPrint(3); // Calls `add2AndPrint`
In this simplified call stack, the sequence is as follows:
β
[add] β Pushed third, pops first
[add2] β Pushed second, pops second
[add2AndPrint] β Pushed first, pops third
β
Each item stored in the call stack is referred to as an execution context.
The execution context stores the following information:
- Variables used inside the function
- The scope chain
- The object that
this
refers to
When a browser executes JavaScript, it manipulates the call stack as follows:
- When the script is loaded, the global execution context is added to the call stack.
- When a function is called, an execution context for that function is created and pushed onto the call stack.
- When a variable is assigned a value, the call stack stores that variable's value.
- When a function finishes executing, it returns the result, and the execution context at the top of the stack is popped off.
- When all the script execution is finished, the global execution context is popped off.
This process allows you to represent complex behavior, like variable assignment and nested function calls, using a simple data structure.
While an execution context exists in the call stack, the browser becomes unresponsive. Since browsers typically run at 60fps, if code execution takes longer than about 16ms, animations might freeze, which negatively impacts user experience.
// A code that loops for a certain time function sleep(milliseconds) { const start = Date.now(); while ((Date.now() - start) < milliseconds); } sleep(5000); // The while loop runs for 5 seconds, blocking the call stack, and the browser becomes unresponsive.
Thus, when writing JavaScript code, especially for user interaction in browsers, itβs important to be mindful of how long the code execution will take.
Task Queue
Not all tasks can be processed within 16ms.
When something requires waiting for an event or a long calculation, it takes time. In such cases, the browser handles these tasks in the following manner:
- Instead of processing the task directly in the JavaScript engine, the task is delegated to the browser via an API, and a callback is registered to be executed once the task completes.
- Once the task is finished, the result and callback are added to the task queue.
- When the call stack is empty, the browser takes the oldest task from the queue and executes its callback. This process is called the event loop.
β
Call Stack β Web APIs β Task Queue β Call Stack (loop)
β
- Tasks in the queue are processed in the order they were added.
- If there are tasks already in the task queue, new tasks will only execute after all prior tasks in the queue have been processed and the call stack is empty.
- If the call stack is not cleared, tasks in the queue cannot be processed.
- Between each task, the browser may repaint the screen. If the call stack isn't cleared, the screen won't be repainted.
Let's revisit the
setTimeout
example with a 0 delay. When the delay is set to 0, the callback isn't executed immediately but is instead added to the task queue. It will only be executed when the call stack is empty, which is why hello
is printed later:setTimeout(() => { console.log('hello'); }, 0); // Callback is added to the task queue console.log('world');
Asynchronous Programming
Asynchronous programming refers to a style of programming where you don't wait for one task to finish before moving on to the next. In contrast, synchronous programming halts execution until the current task is completed.
In the browser, asynchronous programming is often used for tasks like communication with remote servers, which can take time.
Asynchronous programming generally improves a program's performance and responsiveness. However, it has the downside of causing out-of-order execution, which can reduce code readability and make debugging harder. To address these issues, several asynchronous programming techniques have been developed, and some have been incorporated into JavaScript itself. Below are some of the most commonly used asynchronous programming techniques in JavaScript.
Callback
A callback is a function passed as an argument to another function. It is commonly used for asynchronous programming.
const $ = require('jquery'); const API_URL = '<https://api.github.com/repos/facebookincubator/create-react-app/issues?per_page=10>'; $.ajaxSetup({ dataType: 'json' }); $.get(API_URL, issues => { console.log('Recent 10 issues:'); issues .map(issue => issue.title) .forEach(title => console.log(title)); console.log('Finished printing.'); }); console.log('Fetching...');
In the above example, we pass a callback to
$.get
. The method operates asynchronously, delegating the task of fetching data from the Github API to the browser. Once the request is complete, the callback is invoked with the results.It's important to note that not all functions that accept callbacks are asynchronous. For example,
map
and forEach
in the above code execute synchronously, despite accepting callbacks.Callback functions became the most common way to perform asynchronous programming due to JavaScript's support for higher-order functions. However, managing complex asynchronous workflows with callbacks alone can become difficult, leading to what is called callback hell.
Promise
To solve the problems of callbacks, various libraries were created, most notably Promise-based libraries, which were eventually standardized and included in JavaScript as of ES2015.
A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value. A promise can be in one of three states: pending, resolved (fulfilled), or rejected.
Here is an example using Promises:
const p = Promise.resolve(1);
You can chain multiple
.then()
methods to handle successive asynchronous operations:function delay(ms) { return new Promise(resolve => { setTimeout(() => { console.log(`${ms} milliseconds have passed.`); resolve(); }, ms); }); } delay(1000) .then(() => delay(2000)) .then(() => Promise.resolve('Done')) .then(console.log); console.log('Start');
Async Function
While Promises help avoid the callback hell, they still use callbacks, which led to the introduction of async functions in ES2017, allowing you to write asynchronous code in a more synchronous-like manner.
An async function is a function that always returns a Promise. Inside an async function, you can use the
await
keyword to pause execution until a Promise is resolved.async function func1() { return 1; } async function func2() { return Promise.resolve(2); } func1().then(console.log); // 1 func2().then(console.log); // 2
The biggest advantage of async functions is that they allow you to write asynchronous code that looks and behaves like synchronous code, greatly improving readability.
Generator
A generator function allows you to pause and resume function execution.
const co = require('co'); const axios = require('axios'); const API_URL = '<https://api.github.com>'; function* fetchStarCount() { const starCount = {}; // Fetch top repo const topRepoRes = yield axios.get(`${API_URL}/search/repositories?q=language:javascript&sort=stars&per_page=1`); // Fetch top contributors const topMemberRes = yield axios.get(`${API_URL}/repos/${topRepoRes.data.items[0].full_name }/contributors`); starCount.repoName = topRepoRes.data.items[0].name; starCount.stars = topRepoRes.data.items[0].stargazers_count; starCount.topContributor = topMemberRes.data[0].login; return starCount; } co(fetchStarCount).then(console.log);
The advantage of using generator functions is that they give you a more concise way to write asynchronous code.