Let’s design VNode first

The previous chapter covered the nature of components, knowing that the output of a component is a VNode, and the Renderer’s rendering target is also a VNode. It can be seen that VNode is very important in the whole process of framework design. Even designing VNode itself is designing the framework, and the design of VNode will also affect the performance of subsequent algorithms. In this chapter, we will start to design vNodes to try to describe various types of render content.

Use vNodes to describe the real DOM

An HTML tag has its name, attributes, events, styles, child nodes, and many other things that need to be represented in a VNode. We can describe a square div element with a red background as follows:

const elementVNode = {
    tag: 'div'.data: {
        style: {
            width: '100px'.height: '100px'.backgroundColor: 'red'}}}Copy the code

We use the tag attribute to store the name of the tag, and the Data attribute to store additional information about the tag, such as style, class, event, etc. Usually we call the data attribute of a VNode object VNodeData.

To describe child nodes, we need to add the children attribute to the VNode object. The following VNode object describes a div element with child nodes:

const elementVNode = {
    tag: 'div'.data: null.children: {
        tag: 'span'.data: null}}Copy the code

If there are multiple children, we can design the children property as an array:

const elementVNode = {
    tag: 'div'.data: null.children: [{tag: 'h1'.data: null
        },
        {
            tag: 'p'.data: null}}]Copy the code

In addition to label elements, there are text nodes in the DOM. We can use the following VNode object to describe a text node:

const textVNode = {
    tag: null.data: null.children: 'Text content'
}
Copy the code

As above, since the text node has no tag name, its tag attribute value is null. Since text nodes also do not need additional VNodeData to describe additional attributes, their data attribute value is also null.

The only thing to notice is that we use the children property to store the text content of a text node. Some students might ask, “Can I create a new property called text to store text content?”

const textVNode = {
    tag: null.data: null.children: null.text: 'Text content'
}
Copy the code

This is perfectly fine, depending on how you design it, but reusing properties as much as possible with semantics that make sense makes VNode objects lighter, so we chose to use the children property to store text content. Here is a VNode object with a div tag that takes a text node as a child node:

const elementVNode = {
    tag: 'div'.data: null.children: {
        tag: null.data: null.children: 'Text content'}}Copy the code

Using VNodes to describe abstract content

What is abstract content? Components are abstract. For example, if you use a component in a template or JSX, it looks like this:

<div>
    <MyComponent />
</div>
Copy the code

Your intention is not to render a tag element named MyComponent in the page, but to render the content produced by the MyComponent component. But we still need to use vNodes to describe
, and add an identity to the vNodes that describe components, so that when mounted, there is a way to tell whether a VNode is a normal HTML tag or a component.

We can use the following VNode object to describe the above template:

const elementVNode = {
    tag: 'div'.data: null.children: {
        tag: MyComponent,
        data: null}}Copy the code

As mentioned above, the tag value of a VNode used to describe a component refers to the component class (or function) itself, not the tag name string. So in theory, we can determine if a VNode is a normal tag by checking if the tag attribute value is a string.

In addition to components, there are two abstract types of content that need to be described: Fragments and portals. Let’s take a look at what a Fragment is and the problems it solves.

Fragment meaning to render a Fragment, suppose we have the following template:

<template>
    <table>
        <tr>
            <Columns />
        </tr>

    </table>
</template>
Copy the code

The Columns component returns multiple < TD > elements:

<template>
    <td></td>
    <td></td>
    <td></td>
</template>
Copy the code

Consider a question: how to represent the VNode in the template above? If there is only one TD tag in the template, i.e. only one root element, this is easy to express:

const elementVNode = {
    tag: 'td'.data: null
}
Copy the code

But instead of just one TD tag in the template, there are multiple TD tags, i.e. multiple root elements. What does that mean? At this point, we need to introduce an abstract element, the Fragment we will introduce.

const Fragment = Symbol(a)const fragmentVNode = {
    // The tag attribute value is a unique identifier
    tag: Fragment,
    data: null.children: [{tag: 'td'.data: null
        },
        {
            tag: 'td'.data: null
        },
        {
            tag: 'td'.data: null}}]Copy the code

As shown above, we treat all TD tags as children of fragmentVNodes, and the root element is not an actual real DOM, but an abstract identifier, called a Fragment.

When rendering a VNode, if the renderer finds that the type of the VNode is Fragment, it only needs to render the children of the VNode to the page.

In the above code, the value of the fragmentVNode.tag attribute is a unique identifier created through Symbol, but we actually prefer to add a FLAGS attribute to the VNode object that represents the VNode type, as explained later in this chapter.

Let’s look at Portal. What is Portal?

In a word: it allows you to render content anywhere. The application scenario is that if you want to implement a mask component
, the requirement is that the component’s Z-index level is the highest, so that it will cover up everything wherever you use it, you may use it wherever you need the mask.

<template>
    <div id="box" style="z-index: -1;">
        <Overlay />
    </div>
</template>
Copy the code

The unfortunate thing is that, without Portal, the content of the above
component can only be rendered to the div tag with id=”box”, which will inactivate the layers of the mask and possibly affect the layout.

The solution is simple, if the
component renders content that is not restricted by DOM hierarchy and can be rendered anywhere, the problem will be solved.

Using Portal, you can write a template for the
component like this:

<template>
    <Portal target="#app-root">
        <div class="overlay"></div>
    </Portal>
</template>
Copy the code

The net effect is that wherever you use the
component, it will render the content under the element id=”app-root”. Portal renders child nodes to a given destination. We can use the following VNode object to describe this template:

const Portal = Symbol(a)const portalVNode = {
    tag: Portal,
    data: {
        target: '#app-root'
    },
    children: {
        tag: 'div'.data: {
            class: 'overlay'}}}Copy the code

Portal-type VNodes are similar to Fragment vNodes in that they require a unique identifier to distinguish the type and tell the renderer how to render the VNode.

The types of VNode

When vNodes describe different things, their attributes have different values. For example, if a VNode object is a description of an HTML tag, its tag attribute value is a string, the name of the tag. If it is a description of a component, its tag attribute value refers to the component class (or function) itself. If it is a description of a text node, the tag attribute value is null.

It turns out that different types of VNodes have different designs, and those differences add up so that we can categorize them.

In general, vNodes can be divided into five categories: HTML/SVG elements, components, plain text, fragments, and Portal:

As shown in the figure above, components can be subdivided into stateful components and functional components. Stateful components can also be subdivided into three parts: normal stateful components, stateful components that need to be keepAlive, and stateful components that are already keepAlive.

But they are stateful components, whether they are normal stateful components or keepalive-related stateful components. So we can treat them as a class when designing vNodes.

Use flags as VNode identifiers

Since there are categories for vNodes, it is necessary to use a unique identifier to indicate which category a particular VNode belongs to. At the same time, adding flags to VNode is also one of the optimization methods of Virtual DOM algorithm.

For example, Vue2 differentiates vNodes between HTML elements, components, and plain text by doing something like this:

  • 1, to get theVNodeThen try to treat it as a component. If the component is successfully created, then theVNodeThat’s the componentVNode
  • 2. Check if the component was not created successfullyvnode.tagCheck whether there is a definition. If there is a definition, the label is treated as a common label
  • 3, ifvnode.tagIf not, check if it is a comment node
  • 4. If it is not a comment node, it is treated as a text node

\

All the above judgments are made during the mount (or patch) phase. In other words, what exactly a VNode describes is known during the mount or patch phase. This leads to two problems: it can’t be optimized from the AOT level, and developers can’t manually optimize.

In order to solve this problem, we need to specify the VNode type by flags during the creation of the VNode. In this way, we can avoid a lot of performance evaluation by flags during the mount or patch phase. Let’s take a look at the renderer code:

if (flags & VNodeFlags.ELEMENT) {
    // VNode is a common label
    mountElement(/ *... * /)}else if (flags & VNodeFlags.COMPONENT) {
    // VNodes are components
    mountComponent(/ *... * /)}else if (flags & VNodeFlags.TEXT) {
    // VNode is plain text
    mountText(/ *... * /)}Copy the code

As mentioned above, the use of bit operations, in a mount task is likely to be a large number of such judgments, the use of bit operations to a certain extent again improve the runtime performance.

In fact, Vue3 adopts inferno’s method in Virtual DOM optimization. How to do this will be explained in a later chapter.

This means that we should include flags when designing VNode objects:

/ / VNode object
{
    flags:... }Copy the code

Enumeration values VNodeFlags

What are the flags for a VNode object? For each VNode class, we assign a flags value to it. We make it an enumeration value and call it VNodeFlags. In javascript, we use an object to represent it:

const VNodeFlags = {
    / / HTML tags
    ELEMENT_HTML: 1./ / SVG tag
    ELEMENT_SVG: 1 << 1.// Plain stateful components
    COMPONENT_STATEFUL_NORMAL: 1 << 2.// Stateful components that need to be kept alive
    COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE: 1 << 3.// A stateful component that is already keepAlive
    COMPONENT_STATEFUL_KEPT_ALIVE: 1 << 4.// Functional components
    COMPONENT_FUNCTIONAL: 1 << 5./ / plain text
    TEXT: 1 << 6.// Fragment
    FRAGMENT: 1 << 7.// Portal
    PORTAL: 1 << 8
}
Copy the code

The meanings represented by these enumerated attributes can be matched with the following image:Notice that the values of these enumerated properties are basically decimal numbers1To the left by a different number of digits. Based on these basic enumerated property values, we can also derive three additional identifiers:

// HTML and SVG are tag elements, which can be represented by elements
VNodeFlags.ELEMENT = VNodeFlags.ELEMENT_HTML | VNodeFlags.ELEMENT_SVG
Stateful components, stateful components that need to be keepAlive, stateful components that have been keepAlice are all "stateful components".
COMPONENT_STATEFUL = COMPONENT_STATEFULVNodeFlags.COMPONENT_STATEFUL = VNodeFlags.COMPONENT_STATEFUL_NORMAL | VNodeFlags.COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE |  VNodeFlags.COMPONENT_STATEFUL_KEPT_ALIVE// Both stateful and functional components are "components", denoted by COMPONENT
VNodeFlags.COMPONENT = VNodeFlags.COMPONENT_STATEFUL | VNodeFlags.COMPONENT_FUNCTIONAL
Copy the code

Including VNodeFlags. PONENT_STATEFUL ELEMENT, VNodeFlags.COM and VNodeFlags.COM PONENT is from the basic identity through the bitwise or (|) operation, the three derived values will be used for auxiliary judgment.

With these flags in place, we can pre-flag the VNode to indicate the type of the VNode:

// HTML element node
const htmlVnode = {
    flags: VNodeFlags.ELEMENT_HTML,
    tag: 'div'.data: null
}
// SVG element node
const svgVnode = {
    flags: VNodeFlags.ELEMENT_SVG,
    tag: 'svg'.data: null
}
// Functional components
const functionalComponentVnode = {
    flags: VNodeFlags.COMPONENT_FUNCTIONAL,
    tag: MyFunctionalComponent
}
// Plain stateful components
const normalComponentVnode = {
    flags: VNodeFlags.COMPONENT_STATEFUL_NORMAL,
    tag: MyStatefulComponent
}
// Fragment
const fragmentVnode = {
    flags: VNodeFlags.FRAGMENT,
    // Note that since flags exists, we no longer need to use the tag attribute to store unique identifiers
    tag: null
}
// Portal
const portalVnode = {
    flags: VNodeFlags.PORTAL,
    // Note that we no longer need to use the tag attribute to store unique identifiers due to flags. The tag attribute is used to store the target of the Portal
    tag: target
}
Copy the code

Here is an example of using VNodeFlags to determine the VNode type, such as whether a VNode is a component:

// Use bitwise and (&)
functionalComponentVnode.flags & VNodeFlags.COMPONENT / / true
normalComponentVnode.flags & VNodeFlags.COMPONENT / / true
htmlVnode.flags & VNodeFlags.COMPONENT / / false
Copy the code

It’s easy to understand if you’re familiar with bitwise operations. This is actually one of many bitwise tricks. We can make a table:

VNodeFlags Left shift operation 32-BIT sequence of bits (9 bits only for brevity)
ELEMENT_HTML There is no 000000001
ELEMENT_SVG 1 < < 1 000000010
COMPONENT_STATEFUL_NORMAL 1 < < 2 000000100
COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE 1 < < 3 000001000
COMPONENT_STATEFUL_KEPT_ALIVE 1 < < 4 000010000
COMPONENT_FUNCTIONAL 1 < < 5 000100000
TEXT 1 < < 6 001000000
FRAGMENT 1 < < 7 010000000
PORTAL 1 < < 8 100000000

The following table can be easily derived from the base flags values shown in the table above:

VNodeFlags 32-BIT sequence of bits (9 bits only for brevity)
ELEMENT 00000001 1
COMPONENT_STATEFUL 00001 1 100
COMPONENT 0001 1 1 100

So naturally, only vnodeFlags.element_html and vnodeFlags.element_svg with vnodeFlags.element will yield a non-zero value, which is true.

The children and ChildrenFlags

DOM is a tree, and since a VNode is a description of the actual rendered content, it must also be a tree. In the previous design, we defined the children property for vNodes to store child VNodes. How many times can a child node of a tag be? In general, there are no more than the following:

  • No child nodes
  • There is only one child node
  • Multiple child nodes
  • There arekey
  • There is nokey
  • I don’t know what the child nodes are

We can enumerate these cases with an object called ChildrenFlags, which identifies the type of a VNode’s children:

const ChildrenFlags = {
    // Unknown type of children
    UNKNOWN_CHILDREN: 0./ / not children
    NO_CHILDREN: 1.// children is a single VNode
    SINGLE_VNODE: 1 << 1.// Children are vNodes with keys
    KEYED_VNODES: 1 << 2.// Children are multiple vNodes without keys
    NONE_KEYED_VNODES: 1 << 3
}
Copy the code

Since childrenFlags. KEYED_VNODES and childrenFlags. NONE_KEYED_VNODES both belong to multiple VNodes, we can derive a “multi-node” flag to facilitate the program’s determination:

ChildrenFlags.MULTIPLE_VNODES = ChildrenFlags.KEYED_VNODES | ChildrenFlags.NONE_KEYED_VNODES
Copy the code

This makes it much easier to determine if a VNode has multiple children:

someVNode.childFlags & ChildrenFlags.MULTIPLE_VNODES
Copy the code

Why do children need a logo? There’s only one reason: optimization. As you will see later in the chapter on the Diff algorithm, this information is crucial.

In a VNode object, we use the flags attribute to store the VNode type. Similarly, we will use childFlags to store the child node type. Let’s use some practical examples:

// Div tag with no child node
const elementVNode = {
    flags: VNodeFlags.ELEMENT_HTML,
    tag: 'div'.data: null.children: null.childFlags: ChildrenFlags.NO_CHILDREN
}
// The text node's childFlags are always NO_CHILDREN
const textVNode = {
    tag: null.data: null.children: 'I am text'.childFlags: ChildrenFlags.NO_CHILDREN
}
// Have multiple UL tags that use key li tags as child nodes
const elementVNode = {
    flags: VNodeFlags.ELEMENT_HTML,
    tag: 'ul'.data: null.childFlags: ChildrenFlags.KEYED_VNODES,
    children: [{tag: 'li'.data: null.key: 0
        },
        {
            tag: 'li'.data: null.key: 1}}]// Fragment with only one child node
const elementVNode = {
    flags: VNodeFlags.FRAGMENT,
    tag: null.data: null.childFlags: ChildrenFlags.SINGLE_VNODE,
    children: {
        tag: 'p'.data: null}}Copy the code

But not all types of vNodes use the children attribute to store child VNodes. For example, the child VNode of a component should not be children but slots. So we’ll define the vNode. slots attribute to store these child VNodes, but we don’t need to go into slots yet.

VNodeData

As mentioned earlier, VNodeData refers to VNode’s data property, which is an object:

{
    flags:... .tag:... .// VNodeData
    data: {... }}Copy the code

VNodeData, as its name implies, is VNode data used to describe vnodes. For example, if a VNode type is an HTML tag, the VNodeData can contain class, style, and events so that when rendering the VNode, the renderer knows the background color of the tag, the font size, and what events it listens for, etc. So from a design point of view, anything that can be described by a VNode can be stored in a VNodeData object, such as:

{
    flags: VNodeFlags.ELEMENT_HTML,
    tag: 'div'.data: {
        class: ['class-a'.'active'].style: {
            background: 'red'.color: 'green'
        },
        // Other data...}}Copy the code

If VNode is a component, we can also use VNodeData to describe components, such as component events, component props, and so on, assuming the following template:

<MyComponent @some-event="handler" prop-a="1" />
Copy the code

The corresponding VNodeData should be:

{
    flags: VNodeFlags.COMPONENT_STATEFUL,
    tag: 'div'.data: {
        on: {
            'some-event': handler
        },
        propA: '1'
        // Other data...}}Copy the code

Of course, as long as VNode is described correctly, you can design the data structure as you like. Let’s leave the fixed format of VNodeData alone.

In subsequent chapters, we will gradually refine the design of VNodeData based on our requirements.

At this point, we have completed some design of VNode, so far we have designed the VNode object as follows:

export interface VNode {
    The // _isVNode attribute, which was not mentioned above, is a value that is always true and allows us to determine whether an object is a VNode object
    _isVNode: true
    // The el attribute is also not mentioned above. When a VNode is rendered as a real DOM, the el attribute's value references the real DOM
    el: Element | null
    flags: VNodeFlags
    tag: string | FunctionalComponent | ComponentClass | null
    data: VNodeData | null
    children: VNodeChildren
    childFlags: ChildrenFlags
}
Copy the code

The _isVNode attribute and the EL attribute are not mentioned above. The _isVNode attribute is a value that is always true and allows us to determine whether an object is a VNode object. The value of the EL property is null until the VNode is rendered as a real DOM, and when the VNode is rendered as a real DOM, the value of the EL property references the real DOM.

In fact, if you look at the Vue3 source code, you will find that a VNode object contains other attributes such as Handle and contextVNode, parentVNode, key, ref, slots, and so on, in addition to the attributes described in this section.

We didn’t include them in this chapter because we don’t need them at all right now. For example, the Handle property is only used for functional components, so we’ll cover them in the chapter on functional component principles.