Understanding Front End Under the Hood

Unraveling the intricacies of web development, DOM manipulation, and JavaScript interactivity.

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:

  1. Declare a variable called post and assign the value "Hi!" to it.
  2. Call the querySelector method from the document object, passing the argument 'div', and assign the result to jsDiv.
    1. Under the hood, querySelector examines the hidden link, which leads to the C++ object.
    2. It searches for the first element that matches the selector 'div'.
    3. 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.
  3. Use the textContent method (added by querySelector) on jsDiv to set its value to the value of the post variable.
  4. 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:

  1. Declare a variable post and set it as an empty string.
  2. Use the document.querySelector function to obtain a reference to the DOM element and assign it to the jsInput variable.
  3. Similarly, obtain a reference to the <div></div> element using document.querySelector and assign it to the jsDiv variable.
  4. Define the handleInput function.
  5. Assign the handleInput function to the oninput event of the jsInput element.

Now, let’s simulate a user typing "Hi" after 1 minute:

  1. The user types "Hi".
  2. The oninput event is triggered and calls the handleInput function for each character entered.
  3. Within the handleInput function, the post variable is assigned the value of jsInput.
  4. The text content of jsDiv is updated to the value of post.

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:

  1. We declare four variables: name as an empty string, vDOM as the result of calling the createVDOM function, and prevVDOM and elems as uninitialized variables.
  2. We define four functions without calling them: createVDOM, handle, updateDOM, convert, and findDiff. We will examine each of them shortly.
  3. We use the setInterval function from the browser’s window object to run our updateDOM function every 15 milliseconds
  4. Suppose 15 milliseconds have passed, so we call the updateDOM function, which performs the following steps:
    1. Since elems is undefined, we take the if path.
    2. We use the map function on vDOM to convert each element by invoking the convert function.
    3. Inside the convert function, we create a new element called elem.
    4. We set the textContent and value properties of elem to node[1], which will be the value of name.
    5. We set the oninput property of elem to the function returned by handle.
    6. The handle function simply assigns the value of the element to the name variable.
    7. We return the modified elem element and add it to the elems array.
    8. Finally, we append all elements in elems to the document body.
  5. Suppose 1 minute has passed, and the user has typed the letter ‘A’.
    1. After 15 milliseconds, the updateDOM function is called again.
    2. This time, we take the else path since elems is not undefined.
    3. We assign the value of vDOM to prevVDOM.
    4. We regenerate vDOM using the createVDOM function, creating a new Virtual DOM.
    5. We invoke the findDiff function, passing prevVDOM and vDOM as arguments.
      1. Inside the findDiff function, we iterate over the current array’s elements.
      2. We compare the stringified version of prev[i] and current[i] using JSON.stringify.
      3. If the two objects differ, we update the textContent and value of the corresponding element in the elems array with the current values.
  6. 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!