Preface
In a previous discussion, we explored the Vue renderer, where the renderer is primarily responsible for rendering the virtual DOM into the real DOM. We only need to use the virtual DOM to describe the final content to be rendered.
However, when creating more complex pages, the code for the virtual DOM used to describe the page structure becomes larger, or we can say that the page template becomes more extensive. In such cases, the ability to modularize components becomes essential.
With components, we can break down a large page into multiple parts, each part serving as an independent component. These components collectively form the complete page. The implementation of components also requires support from the renderer.
Rendering Components
From the user’s perspective, a stateful component is simply an options object, as shown in the code below:
// MyComponent is a component, and its value is an options object
const MyComponent = {
name: 'MyComponent',
data() {
return { foo: 1 }
}
}
However, from the internal implementation of the renderer, a component is a special type of virtual DOM node. For example, to describe a regular tag, we use the vnode.type property of the virtual node to store the tag name, as shown in the code below:
// This vnode is used to describe a regular tag
const vnode = {
type: 'div'
// ...
}
To describe a fragment, we set the vnode.type property of the virtual node to ‘Fragment’, and to describe text, we set the vnode.type property to ‘Text’.
Do you recall the patch function
we discussed earlier in the renderer?
In the patch function, Vue processes different logic based on the differences between the new and old virtual DOM nodes. If it’s the initial rendering, it executes the logic for the initial rendering; if it’s an update rendering, it updates the actual DOM nodes based on the differences.
function patch(n1, n2, container, anchor) {
// 02. If the types of new and old nodes are different, perform unmount operation
if (n1 && n1.type !== n2.type) {
unmount(n1);
n1 = null;
}
// 07. Get the node type
const { type } = n2;
// 09. Check the node type
if (typeof type === 'string') {
// 10. Process regular elements
// TODO: Logic for regular elements
} else if (type === Text) {
// 11. Process text nodes
// TODO: Logic for text nodes
} else if (type === Fragment) {
// 13. Process fragments
// TODO: Logic for fragments
}
// Return the actual DOM element of the new node
return n2.el;
}
As you can see, the renderer uses the vnode.type property of the virtual node to differentiate its type. Different types of nodes require different methods for mounting and updating.
In reality, this applies to components as well. To use virtual nodes to describe components, we can use the vnode.type property to store the component’s options object.
function patch(n1, n2, container, anchor) {
// 02. If the types of new and old nodes are different, perform unmount operation
if (n1 && n1.type !== n2.type) {
unmount(n1);
n1 = null;
}
// 07. Get the node type
const type = n2.type;
// 09. Check the node type
if (typeof type === 'string') {
// 10. Process regular elements
// TODO: Logic for regular elements
} else if (type === Text) {
// 11. Process text nodes
// TODO: Logic for text nodes
} else if (type === Fragment) {
// 13. Process fragments
// TODO: Logic for fragments
} else if (typeof type === 'object') {
// 16. The value of vnode.type is an options object, treated as a component
if (!n1) {
// 17. Mount the component
mountComponent(n2, container, anchor);
} else {
// 21. Update the component
patchComponent(n1, n2, anchor);
}
}
// Return the actual DOM element of the new node
return n2.el;
}
In the above code, we added an else if branch to handle the case where the vnode.type property value is an object, treating it as a virtual node describing a component. We call the mountComponent and patchComponent functions to mount and update the component.
Now let’s focus on the basics of writing components—what the user should write when creating components, what the options object of a component must include, and what capabilities a component possesses.
Therefore, a component must include a rendering function, i.e., the render function, and the return value of the render function should be a virtual DOM. In other words, the render function of a component is an interface for describing the content that the component renders, as shown in the code below:
// Definition of the MyComponent component
const MyComponent = {
// Component name, optional
name: 'MyComponent',
// Rendering function of the component, its return value must be a virtual DOM
render() {
// Return a virtual DOM
return {
type: 'div',
children: ['text content']
};
}
};
// Vnode object describing the component, type property value is the options object of the component
const CompNode = {
type: MyComponent
};
// Call the renderer to render the component
renderer.render(CompNode, document.querySelector('#app'));
// The mountComponent function in the renderer is responsible for the actual rendering of the component
function mountComponent(vnode, container, anchor) {
// Get the options object of the component, i.e., vnode.type
const componentOptions = vnode.type;
// Get the rendering function of the component, render
const render = componentOptions.render;
// Execute the rendering function, get the content the component should render, i.e., the virtual DOM returned by the render function
const subTree = render();
// Finally, call the patch function to mount the content described by the component, i.e., subTree
patch(null, subTree, container, anchor);
}
Conclusion
Let’s recap the basics of components. We explicitly declared an object instance for a component, and the options object of the component must include a rendering function, i.e., the render function. The return value of the render function should be a virtual DOM. We then call render and patch to update and replace components. **In other words, the render function of a component is an interface for describing
Comments