If you have any thoughts on my blog or articles and you want to let me know, you can either post a comment below(public) or tell me via this i_kkkp@163.com

vue-renderer-1

Preface

In Vue.js, many functionalities rely on renderers to be implemented, such as Transition components, Teleport components, Suspense components, as well as template refs and custom directives.

Moreover, the renderer is the core of the framework’s performance, as its implementation directly affects the framework’s performance. Vue.js 3’s renderer not only includes the traditional Diff algorithm but also introduces a fast path update method, leveraging the information provided by the compiler, significantly improving update performance.

In Vue.js, the renderer is responsible for executing rendering tasks. On the browser platform, it renders the virtual DOM into real DOM elements. The renderer can render not only real DOM elements but also plays a key role in the framework’s cross-platform capabilities. When designing a renderer, its customizable capabilities need to be considered.

Basic Concepts and Meanings of Renderer

Before implementing a basic renderer, we need to understand a few fundamental concepts:

In Vue.js, a renderer is a component responsible for rendering virtual DOM (or virtual nodes) into real elements on a specific platform. On the browser platform, the renderer renders virtual DOM into real DOM elements.

Renderer

The renderer is responsible for rendering virtual DOM (or virtual nodes) into real elements on a specific platform. On the browser platform, the renderer renders virtual DOM into real DOM elements.

Virtual DOM (vnode)

The virtual DOM (also known as virtual nodes, abbreviated as vnode) is a tree-like structure, similar to real DOM, consisting of various nodes. The renderer’s task is to render the virtual DOM into real DOM elements.

Mounting

Mounting refers to rendering the virtual DOM into real DOM elements and adding them to a specified mounting point. In Vue.js, the mounted lifecycle hook of a component is triggered when the mounting is completed, and it can access the real DOM element at this point.

Container

The container specifies the mounting position’s DOM element. The renderer renders the virtual DOM into real DOM elements and adds them to the specified container. In the renderer’s render function, a container parameter is usually passed in, indicating which DOM element the virtual DOM is mounted to.

Creation and Usage of Renderer

The renderer is usually created using the createRenderer function, which returns an object containing rendering and hydration functions. The hydration function is used in server-side rendering (SSR) to hydrate virtual DOM into existing real DOM elements. Here’s an example of creating and using a renderer:

function createRenderer() {
  function render(vnode, container) {
    // Render logic
  }

  function hydrate(vnode, container) {
    // Hydration logic
  }

  return {
    render,
    hydrate
  };
}

const { render, hydrate } = createRenderer();
// Initial rendering
render(vnode, document.querySelector('#app'));
// Server-side rendering
hydrate(vnode, document.querySelector('#app'));

In the above code, the createRenderer function creates a renderer object that contains the render and hydrate functions. The render function is used to render the virtual DOM into real DOM elements, while the hydrate function is used to hydrate the virtual DOM into existing real DOM elements.

Now that we have a basic understanding of the renderer, let’s dive deeper step by step.

The implementation of the renderer can be represented by the following function, where domString is the HTML string to be rendered, and container is the DOM element to mount to:

function renderer(domString, container) {
  container.innerHTML = domString;
}

Example usage of the renderer:

renderer('<h1>Hello</h1>', document.getElementById('app'));

In the above code, <h1>Hello</h1> is inserted into the DOM element with the id app. The renderer can not only render static strings but also dynamic HTML content:

let count = 1;
renderer(`<h1>${count}</h1>`, document.getElementById('app'));

If count is a reactive data, the reactivity system can automate the entire rendering process. First, define a reactive data count and then call the renderer function inside the side effect function to render:

const count = ref(1);

effect(() => {
  renderer(`<h1>${count.value}</h1>`, document.getElementById('app'));
});

count.value++;

In the above code, count is a ref reactive data. When modifying the value of count.value, the side effect function will be re-executed, triggering re-rendering. The final content rendered to the page is <h1>2</h1>.

Here, the reactive API provided by Vue 3’s @vue/reactivity package is used. It can be included in the HTML file using the <script> tag:

<script src="https://unpkg.com/@vue/reactivity@3.0.5/dist/reactivity.global.js"></script>

The basic implementation of the render function is given in the above code. Let’s analyze its execution flow in detail. Suppose we call the renderer.render function three times consecutively for rendering:

const renderer = createRenderer();

// Initial rendering
renderer.render(vnode1, document.querySelector('#app'));
// Second rendering
renderer.render(vnode2, document.querySelector('#app'));
// Third rendering
renderer.render(null, document.querySelector('#app'));

During the initial rendering, the renderer renders vnode1 into real DOM and stores vnode1 in the container element’s container.vnode property as the old vnode.

During the second rendering, the old vnode exists (container.vnode has a value), and the renderer takes vnode2 as the new vnode, passing both the new and old vnodes to the patch function to perform patching.

During the third rendering, the new vnode’s value is null, indicating that no content should be rendered. However, at this point, the container already contains the content described by vnode2, so the renderer needs to clear the container. In the code above, container.innerHTML = '' is used to clear the container. It’s important to note that clearing the container this way is not the best practice but is used here for demonstration purposes.

Regarding the patch function, it serves as the core entry point of the renderer. It takes three parameters: the old vnode n1, the new vnode n2, and the container container. During the initial rendering, the old vnode n1 is undefined, indicating a mounting action. The patch function not only serves for patching but can also handle mounting actions.

Custom Renderer

The implementation of a custom renderer involves abstracting the core rendering logic, making it independent of specific platform APIs. The following example demonstrates the implementation of a custom renderer using configuration options to achieve platform-independent rendering:

// Create a renderer function, accepting options as parameters
function createRenderer(options) {
  // Retrieve DOM manipulation APIs from options
  const { createElement, insert, setElementText } = options;

  // Define the function to mount elements
  function mountElement(vnode, container) {
    // Call createElement function to create an element
    const el = createElement(vnode.type);
    // If children are a string, use setElementText to set text content
    if (typeof vnode.children === 'string') {
      setElementText(el, vnode.children);
    }
    // Call insert function to insert the element into the container
    insert(el, container);
  }

  // Define the function to patch elements
  function patch(n1, n2, container) {
    // Implement patching logic, this part is omitted in the example
  }

  // Define the render function, accepting virtual nodes and a container as parameters
  function render(vnode, container) {
    // If the old virtual node exists, execute patching logic; otherwise, execute mounting logic
    if (container.vnode) {
      patch(container.vnode, vnode, container);
    } else {
      mountElement(vnode, container);
    }
    // Store the current virtual node in the container's vnode property
    container.vnode = vnode;
  }

  // Return the render function
  return render;
}

// Create configuration options for the custom renderer
const customRendererOptions = {
  // Function for creating elements
  createElement(tag) {
    console.log(`Creating element ${tag}`);
    // In a real application, you can return a custom object to simulate a DOM element
    return { type: tag };
  },
  // Function for setting an element's text content
  setElementText(el, text) {
    console.log(`Setting text content of ${JSON.stringify(el)}: ${text}`);
    // In a real application, set the object's text content
    el.textContent = text;
  },
  // Function for inserting an element under a given parent
  insert(el, parent, anchor = null) {
    console.log(`Adding ${JSON.stringify(el)} to ${JSON.stringify(parent)}`);
    // In a real application, insert el into parent
    parent.children = el;
  },
};

// Create a render function using the custom renderer's configuration options
const customRenderer = createRenderer(customRendererOptions);

// Create a virtual node describing <h1>hello</h1>
const vnode = {
  type: 'h1',
  children: 'hello',
};

// Use an object to simulate a mounting point
const container = { type: 'root' };

// Render the virtual node to the mounting point using the custom renderer
customRenderer(vnode, container);

In the above code, we create a custom renderer using the createRenderer function, which takes configuration options as parameters. The configuration options include functions for creating elements, setting text content, and inserting elements into a parent. The customRenderer function takes a virtual node and a container as parameters, and it can handle both mounting and patching logic based on the existence of the old virtual node (container.vnode).

This custom renderer allows platform-independent rendering by abstracting the core logic and making it adaptable to different platforms through configuration options.

Please note that the code above demonstrates the concept of a custom renderer and focuses on its implementation logic. In a real-world scenario, you might need additional error handling, optimizations, and proper DOM manipulations based on the specific platform requirements.

Vue Render Mounting and Updating How Does JIT (Just-In-Time) Compiler Work

Comments