In web development, we typically have two main goals: displaying the desired content to users and enabling them to interact with it by modifying the data they see. However, dealing with the various components of web browsers can present significant challenges when it comes to displaying content at any scale. Web browsers are large and complex software programs that have evolved over the past 30 years to handle a wide range of content types, from text to videos.
To achieve our goals, we need certain technologies and knowledge, such as JavaScript, DOM, WebCore, WebIDL, and Event API. I will explain each one of them in the course of this article.
So what is DOM? DOM stands for Document Object Model and is a programming interface for web documents. It represents the document (page) as nodes and objects and gives access to it through an API, making it possible for programming languages to interact with the page. The DOM is designed to be independent of any language, so it’s not part of JavaScript, just accessed by it via the API. Other languages, such as PHP and Python can also access DOM, but we frequently use JavaScript because it is the language that runs in the browser.
It’s also important to notice that the DOM represents a document with a logical tree, and each branch of the tree ends in a node. Each node contains objects. In this article, I will represent it as a list of objects to make the explanation easier.
Unlike other programming areas, User Interface Engineering has a visual output. We can observe the pixels on the screen as a result of what we do. However, displaying the pixels one by one is a lot of work and not productive. So, we use some languages to handle this task, and the most commonly used language to display content is HTML because it provides a descriptive and intuitive way to present what we want on the screen.
For instance, in HTML, we can order the elements to specify which of them should appear first on the screen, like this:
What's your name?
<input />
<button>Click!</button>
This HTML text is parsed via tokenization and tree construction, generating a Document object (implemented in C++). The browser then uses this list of objects to display the elements in the order they were added. The DOM enables us to interact with the page without the need to manage every individual pixel.
By accomplishing this, we have achieved our first goal: displaying content. However, we still need to enable user interaction, which can only be accomplished through JavaScript.
And we have some reasons to choose JavaScript here. It is the only language that can interact with the C++ list of elements. Also, JavaScript files can be added to HTML using the script tag, and JavaScript has a call stack and an event loop that allow code to run asynchronously. All these reasons have made JavaScript one of the most popular languages today.
If we want to display data using JavaScript, we will need WebIDL and WebCore.
According to MDN, WebIDL (Web Interface Description Language) is "the interface description language used to describe the data types, interfaces, methods, properties, and other components that make up a Web application programming interface." It precisely describes the elements of the DOM.
WebCore, as defined by WebIDL, provides access to the DOM, and the exact position where we add a node to the DOM is determined by the execution of our associated main runtime in JavaScript. WebCore also enables JavaScript to access the DOM and manipulate pixels.
We need this because it is not possible to directly modify the DOM using C++. Instead, we need to use functions like querySelector
, which automatically creates an object with a hidden link (even hidden from console.log
) to the real DOM element. Through this link, we can access a set of useful functions to edit, change, remove, update, or retrieve information from the C++ object in the DOM. Then, querySelector
returns these useful functions as a result.
Here’s an example HTML code snippet:
<!-- app.html -->
<input />
<div></div>
<script src="app.js"></script>
And the corresponding JavaScript code snippet:
// app.js
// document - obj of methods for DOM access
let post = "Hi!";
const jsDiv = document.querySelector("div"); // This list of functions allows us to access the list of elements in C++
jsDiv.textContent = post;
console.log(jsDiv) // misrepresentative
Let’s break down the JavaScript code above:
- Declare a variable called
post
and assign the value"Hi!"
to it. - Call the
querySelector
method from thedocument
object, passing the argument'div'
, and assign the result tojsDiv
.- Under the hood,
querySelector
examines the hidden link, which leads to the C++ object. - It searches for the first element that matches the selector
'div'
. - It automatically creates an object with a hidden link to the original
div
element in the C++ object and populates this object with a set of methods to help us handle this element. It’s important to notice that the object we create it’s not the same as the C++ object, it’s only a representation with methods to change it.
- Under the hood,
- Use the
textContent
method (added byquerySelector
) onjsDiv
to set its value to the value of thepost
variable. - Log
jsDiv
to the console, which will display<div>Hi!</div>
.
As we can see, modifying the DOM with JavaScript is more challenging and less intuitive than using HTML. However, we still use JavaScript because it provides us with more control over the DOM and enables interactivity on the page. In addition, HTML is primarily used for one-time descriptions, and it lacks the tools to interact with HTML data on the page.
With this knowledge, let’s examine the JavaScript code below and understand how it achieves interactivity:
let post = "";
const jsInput = document.querySelector("input");
const jsDiv = document.querySelector("div");
function handleInput() {
post = jsInput.value;
jsDiv.textContent = post;
}
jsInput.oninput = handleInput;
This code allows us to achieve interactivity by performing the following steps:
- Declare a variable
post
and set it as an empty string. - Use the
document.querySelector
function to obtain a reference to the DOM element and assign it to thejsInput
variable. - Similarly, obtain a reference to the
<div></div>
element usingdocument.querySelector
and assign it to thejsDiv
variable. - Define the
handleInput
function. - Assign the
handleInput
function to theoninput
event of thejsInput
element.
Now, let’s simulate a user typing "Hi" after 1 minute:
- The user types "Hi".
- The
oninput
event is triggered and calls thehandleInput
function for each character entered. - Within the
handleInput
function, thepost
variable is assigned the value ofjsInput
. - The text content of
jsDiv
is updated to the value ofpost
.
Following these steps, the <div>
element will display the value "Hi," achieving our goal of interactivity in the browser. This functionality is possible thanks to JavaScript and its integration with the DOM object in C++.
It’s important to note that the data is not stored in the JavaScript object itself. Instead, information is exchanged between JavaScript and the C++ DOM object using getters and setters.
So the two main goals have been accomplished: we showed data to the users and allowed them to change it. However, in the process, we have added complexity to our code, making it non-scalable and consuming excessive memory space. To address the scalability issue, we can implement one-way data binding, which is a paradigm where the data flows in a single direction, from the source to the target. This is similar to components with children in React, where the data flows in a "top-down" direction. Adopting this paradigm will help us organize our code, as well as make it scalable, easier to debug, and more predictable. It is also used in popular frameworks like Angular, Vue, and React.
To ensure optimal performance, we need to restrict changes to the view by updating the data and running a single converter function. Additionally, we can create a Virtual DOM to identify where the data has changed and update only those specific areas, thus optimizing resource utilization.
But what exactly is the Virtual DOM? It is essentially a representation of the real DOM stored in the user’s memory. We make changes to the Virtual DOM first, compare it with the real DOM, and update only the portions that have changed. This approach helps us save resources and time when updating the data on the user’s page.
In the provided code, we represent the DOM in JavaScript by using blocks of code, with each block representing a view element. The order in which we place these elements in our list determines their position on the page, much like HTML.
let name = ""; let vDOM = createVDOM(); let prevVDOM; let elems; // 1
function createVDOM() { // 2
return [
["input", name, handle],
["div", `Hello, ${name}!`],
];
}
function handle(e) { // 2
name = e.target.value; // 4.5, 4.6
}
function updateDOM() { // 2, 5.1
if (elems === undefined) { // 4.1
elems = vDOM.map(convert); // 4.2
document.body.append(...elems); // 4.8
} else { // 5.2
prevVDOM = [...vDOM]; // 5.3
vDOM = createVDOM(); // 5.4
findDiff(prevVDOM, vDOM); // 5.5
}
}
function convert(node) { // 2
const elem = document.createElement(node[0]); // 4.3
elem.textContent = node[1]; // 4.4
elem.value = node[1]; // 4.4
elem.oninput = node[2]; // 4.5
return elem; // 4.7
}
function findDiff(prev, current) { // 2
for (let i = 0; i < current.length; i++) { // 5.1
if (JSON.stringify(prev[i]) !== JSON.stringify(current[i])) { // 5.2
elems[i].textContent = current[i][1]; // 5.3
elems[1].value = current[i][1]; // 5.3
}
}
}
setInterval(updateDOM, 15); // 3, 6
Let’s break down this code:
- We declare four variables:
name
as an empty string,vDOM
as the result of calling thecreateVDOM
function, andprevVDOM
andelems
as uninitialized variables. - We define four functions without calling them:
createVDOM
,handle
,updateDOM
,convert
, andfindDiff
. We will examine each of them shortly. - We use the
setInterval
function from the browser’swindow
object to run ourupdateDOM
function every 15 milliseconds - Suppose 15 milliseconds have passed, so we call the
updateDOM
function, which performs the following steps:- Since
elems
is undefined, we take theif
path. - We use the
map
function onvDOM
to convert each element by invoking theconvert
function. - Inside the
convert
function, we create a new element calledelem
. - We set the
textContent
andvalue
properties ofelem
tonode[1]
, which will be the value ofname
. - We set the
oninput
property ofelem
to the function returned byhandle
. - The
handle
function simply assigns the value of the element to thename
variable. - We return the modified
elem
element and add it to theelems
array. - Finally, we append all elements in
elems
to thedocument body
.
- Since
- Suppose 1 minute has passed, and the user has typed the letter ‘A’.
- After 15 milliseconds, the
updateDOM
function is called again. - This time, we take the
else
path sinceelems
is notundefined
. - We assign the value of
vDOM
toprevVDOM
. - We regenerate
vDOM
using thecreateVDOM
function, creating a new Virtual DOM. - We invoke the
findDiff
function, passingprevVDOM
andvDOM
as arguments.- Inside the
findDiff
function, we iterate over thecurrent
array’s elements. - We compare the stringified version of
prev[i]
andcurrent[i]
usingJSON.stringify
. - If the two objects differ, we update the
textContent
andvalue
of the corresponding element in theelems
array with the current values.
- Inside the
- After 15 milliseconds, the
- This process repeats every 15 milliseconds, updating the current DOM based on changes made.
After all of this, if the user types the letter ‘A’, it will be displayed in the <div>
below the input as "Hello, A!".
So this is basically how Front End works under the hood. You can check the references for more content or get in touch if you have any questions.
References:
Hard Parts UI
DOM by MDN
HTML parsing
setInterval
We want to work with you. Check out our "What We Do" section!