How does vNode transition to the real DOM?

What is a vnode

A VNode is essentially a JavaScript object that describes the DOM. It can describe different types of nodes in vue.js, such as common element nodes, component nodes, and so on.

Common element nodes are familiar. For example, we define a button tag in HTML to write a button:

    <button class="btn" style="color: blue">I'm a button</button>
Copy the code

Correspondingly, we can use vnode to represent the button tag

const vnode = {
  type: 'button'.props: {
    class: 'btn'.style: {
      color: 'blue',}},children: 'I'm a button.'
}
Copy the code

The type attribute represents the tag type of the DOM, the props attribute represents some DOM attachment information, such as style, class, etc., and the children attribute represents a child node of the DOM, which can also be a VNode array.

What is a component

A component is an abstraction of a DOM tree.

For example, we now define a component node on the page:

 <my-component></my-componnet>
Copy the code

This code does not render a my-Component tag on the page. What it renders depends on how you write the template for the MyComponent. For example, the template definition inside the MyComponent looks like this:

<template>
  <div>
    <h2>I'm a component</h2>
  </div>
</template>
Copy the code

As you can see, the template ends up rendering a div on the page with an H2 tag inside that shows I’m a component text.

So, in terms of representation, the component’s template determines the DOM tag that the component generates, and inside vue.js, a component that actually renders the DOM needs to go through the “create VNode-render vNode-generate DOM” steps

So what does vNode have to do with components?

We now know that a VNode can be used to describe an object in the real DOM, and it can also be used to describe components. For example, we introduce a component tag custom-Component in the template:

<custom-component msg="test"></custom-component>
Copy the code

We can represent custom-component component tags with vnode as follows:

const CustomComponent = {
  // Define the component object here
}
const vnode = {
  type: CustomComponent,
  props: {
    msg: 'test'}}Copy the code

The component VNode is really a description of something abstract, because we don’t actually render a custom-Component tag on the page, but an HTML tag defined inside the component.

Core rendering process: Create and render a VNode

When vue3 initialses an app, it creates an app object. The app.mount function is created as follows:

mount(rootContainer, isHydrate, isSVG) {
    // Create a vNode for the root component
    const vnode = createVNode(rootComponent, rootProps)
    / / render vnode
    render(vnode, rootContainer, isSVG);
}
Copy the code

Create a vnode

Vnodes are created using the function createVNode. Let’s look at the rough implementation of this function:

function createVNode(type, props = null ,children = null) {
    // If the type passed in is itself a vnode, clone the vnode and return
    if (isVNode(type)) {
        const cloned = cloneVNode(type, props, true /* mergeRef: true */);
        if (children) {
            // Convert children of different data types into arrays or text types
            normalizeChildren(cloned, children);
        }
        return cloned;
    }
  if (props) {
    // handle props logic, standardize class and style
  }

  // Encode vNode type information
  const shapeFlag = isString(type)
    ? 1 /* ELEMENT */
    : isSuspense(type)
      ? 128 /* SUSPENSE */
      : isTeleport(type)
        ? 64 /* TELEPORT */
        : isObject(type)
          ? 4 /* STATEFUL_COMPONENT */
          : isFunction(type)
            ? 2 /* FUNCTIONAL_COMPONENT */
            : 0

    const vnode = {
        type,
        props,
        shapeFlag,
        // Some other attributes
    }
    // Convert children of different data types into arrays or text types
    normalizeChildren(vnode, children)
    return vnode
}
Copy the code

CreateVNode does a simple thing: normalize props, code vnode types, createVNode objects, and standardize children.

Now that we have the VNode object, all we need to do is render it into the page.

Rendering vnode

Next, the vNode is rendered. Let’s look at the render function implementation:

const render = (vnode, container, isSVG) = > {
    if (vnode == null) {
        // Destroy the component
        if (container._vnode) {
            unmount(container._vnode, null.null.true); }}else {
        // Create or update the component
        patch(container._vnode || null, vnode, container, null.null.null, isSVG);
    }
    // Invoke the callback scheduler
    flushPostFlushCbs();
    // Cache the vNode node, indicating that it has been rendered
    container._vnode = vnode;
};
Copy the code

The implementation of the render function is simple: if its first argument, vnode, is null, the logic to destroy the component is executed; otherwise, the logic to create or update the component is executed. We’ll ignore flushPostFlushCbs for now and look at what it does when we analyze the nextTick principle.

path vnode

Next, let’s look at the implementation of the path function that creates or updates component nodes:

    const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = isHmrUpdating ? false:!!!!! n2.dynamicChildren) = > {
        // Same node
        if (n1 === n2) {
            return;
        }
        // If there are old and new nodes, and the types of the old and new nodes are different, the old node is destroyed
        if(n1 && ! isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1); unmount(n1, parentComponent, parentSuspense,true);
            n1 = null;
        }

        switch (type) {
            case Text:
                // Process text nodes
                processText(n1, n2, container, anchor);
                break;
            case Comment$1:
                // Process the comment node
                processCommentNode(n1, n2, container, anchor);
                break;
            case Static:
                // Handle static nodes
                if (n1 == null) {
                    mountStaticNode(n2, container, anchor, isSVG);
                }
                else {
                    patchStaticNode(n1, n2, container, isSVG);
                }
                break;
            case Fragment:
                 // Process the Fragment element
                processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
                break;
            default:
                if (shapeFlag & 1 /* ELEMENT */) {
                    // Handle normal DOM elements
                    processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
                }
                else if (shapeFlag & 6 /* COMPONENT */) {
                    // Process components
                    processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
                }
                else if (shapeFlag & 64 /* TELEPORT */) {
                    / / processing TELEPORT
                    type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals);
                }
                else if (shapeFlag & 128 /* SUSPENSE */) {
                    / / deal with SUSPENSE
                    type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals);
                }
                else {
                    warn$1('Invalid VNode type:', type, ` (The ${typeof type}) `); }}/ / ref
        if(ref ! =null && parentComponent) {
            setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2);
        }
    };
Copy the code

I’m trying to patch it up. This function has two functions:

  • Mount the DOM according to the VNode
  • Update the DOM based on the old and new VNodes

For the first rendering, we will only analyze the creation process here, and the update process will be analyzed in the following sections.

In the process of creation, the patch function accepts multiple parameters. Here, we only focus on the first three:

  1. The first parameter n1 indicates the old vNode. When n1 is null, it indicates a mount process.

  2. The second parameter, n2, represents the new vNode, and different processing logic will be executed later depending on the vNode type.

  3. The third parameter container represents the DOM container. After the DOM is rendered by vNode, it is mounted under the Container.

For rendered nodes, we focus here on rendering logic for two types of nodes: processing of components and processing of ordinary DOM elements.

Path: the component

Let’s look at the implementation of the processComponent function that handles the component:

const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) = > {
    n2.slotScopeIds = slotScopeIds;
    if (n1 == null) {
        // Mount the component
        mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
    } else {
        // Update the componentupdateComponent(n1, n2, optimized); }};Copy the code

The logic of this function is simple: if n1 is null, the logic of mounting the component is performed; otherwise, the logic of updating the component is performed.

Let’s look at the implementation of the mountComponent function that mounts the component:

    const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) = > {
        // Create a component instance
        const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense));

        if (isKeepAlive(initialVNode)) {
            // Component cache
            instance.ctx.renderer = internals;
        }
        // Set up component instances, such as props and slots
        setupComponent(instance);
        if (instance.asyncDep) {
            // Asynchronous components
            parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect);
            if(! initialVNode.el) {const placeholder = (instance.subTree = createVNode(Comment$1));
                processCommentNode(null, placeholder, container, anchor);
            }
            return;
        }
        // Set up and run the render function with side effects
        setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized);
    };
Copy the code

As you can see, the mountComponent function does three main things: create a component instance, set up a component instance, and set up and run a rendering function with side effects.

Where, if there is a component cache, the instance’s renderer is replaced. This is all you need to know for now, but we will explore this further when we examine component caching mechanisms later. Asynchronous component processing is similar, and will be examined in more detail when analyzing dynamic components later.

Creating a component instance

Let’s look at the implementation of createComponentInstance:

  function createComponentInstance(vnode, parent, suspense) {
      const type = vnode.type;
      // The component's context is its own if it is the root component, otherwise it inherits the parent component's context
      const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
      const instance = {
          vnode,
          type,
          parent,
          appContext,
          root: null.next: null.subTree: null.update: null.scope: new EffectScope(true /* detached */),
          render: null.provides: parent ? parent.provides : Object.create(appContext.provides),
          renderCache: [].// local resovled assets
          components: null.directives: null.// resolved props and emits options
          propsOptions: normalizePropsOptions(type, appContext),
          emitsOptions: normalizeEmitsOptions(type, appContext),
          // emit
          emit: null.emitted: null.// props default value
          propsDefaults: EMPTY_OBJ,
          // inheritAttrs
          inheritAttrs: type.inheritAttrs,
          // state
          ctx: EMPTY_OBJ,
          data: EMPTY_OBJ,
          props: EMPTY_OBJ,
          attrs: EMPTY_OBJ,
          slots: EMPTY_OBJ,
          refs: EMPTY_OBJ,
          setupState: EMPTY_OBJ,
          setupContext: null.// suspense related
          suspense,
          suspenseId: suspense ? suspense.pendingId : 0.asyncDep: null.asyncResolved: false.// lifecycle hooks
          // not using enums here because it results in computed properties
          isMounted: false.isUnmounted: false.isDeactivated: false.bc: null.c: null.bm: null.m: null.bu: null.u: null.um: null.bum: null.da: null.a: null.rtg: null.rtc: null.ec: null.sp: null
      };
      instance.root = parent ? parent.root : instance;
      instance.emit = emit.bind(null, instance);
      // apply custom element special handling
      if (vnode.ce) {
          vnode.ce(instance);
      }
      return instance;
  }
Copy the code

As you can see, an instance of a component is a JS object, which contains many properties, consistent with the abstract concept of a component we introduced earlier.

Setting up component instances

Once the component instance is created, I’ll look at the implementation of the setupComponent function:

  function setupComponent(instance, isSSR = false) {
      isInSSRComponentSetup = isSSR;
      const { props, children } = instance.vnode;
      const isStateful = isStatefulComponent(instance);
      // Handle the props of the component
      initProps(instance, props, isStateful, isSSR);
      // Handle component slots
      initSlots(instance, children);
      // Handle other component properties, such as creating the render function
      const setupResult = isStateful
          ? setupStatefulComponent(instance, isSSR)
          : undefined;
      isInSSRComponentSetup = false;
      return setupResult;
  }
Copy the code

SetupComponent sets up component initialization data, such as props, slots, Render, and so on.

Set up and run render functions with side effects

Finally, let’s look at the implementation of running setupRenderEffect with side effects:

const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) = > {

    const componentUpdateFn = () = > {
        // omit the life hook
      if(! instance.isMounted) {if (el && hydrateNode) {
            / / omit hydrateNode
        } else {
          // Render component generates subtree vNodes
          const subTree = (instance.subTree = renderComponentRoot(instance))
          // Mount the subtree vNode to the container
          patch(null,subTree,container,anchor,instance,parentSuspense,sSVG)
          // Preserve the child root DOM nodes generated by rendering
          initialVNode.el = subTree.el
        }
        // omit the life hook
        instance.isMounted = true
      } else {
        // Update the component}}// Create a reactive side effect rendering function
    const effect = new ReactiveEffect(
      componentUpdateFn,
      () = > queueJob(instance.update),
      instance.scope // track it in component's effect scope
    )
    // Manually bind this to the side effect rendering function
    const update = (instance.update = effect.run.bind(effect) as SchedulerJob)

    // Execute the side effect rendering function
    update()
}

Copy the code

The setupRenderEffect function has so many responsibilities that we are now analyzing only the initial rendering process, omitting the other logic. Examples include component lifecycle, hydrateNode, logic for updating components, and so on.

In addition, creating reactive side effects functions can be very abstract. It is easy to understand that instantiating a ReactiveEffect produces the side effect effcet function. The side effect, which you can simply interpret as “componentEffect”, is that when the component’s data changes, the effect function is wrapped around the internal rendering function componentEffect, which re-renders the component. We will explore reactiveeffects further when we read the reactive correlation code.

The initial rendering does two things: the rendering component generates the subTree and mounts the subTree into the Container.

RenderComponentRoot executes the render function to create vNodes inside the entire component tree. RenderComponentRoot executes the render function to create vNodes inside the entire component tree. Standardizing the vNode through an internal layer gives the result that this function returns: a subtree vNode.

After rendering the subtree vNode, the next step is to call the patch function to mount the subtree vnode to the Container.

Path: Normal DOM element

After analyzing the flow of the PATH component, let’s look at the implementation of the processElement function for the path normal DOM element:

const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) = > {
    isSVG = isSVG || n2.type === 'svg';
    if (n1 == null) {
        // // Mount the element node
        mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
    } else {
        // Update the element nodepatchElement(n1, n2, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized); }};Copy the code

The logic is similar to the processComponent function: if n1 is null, mount element node logic, otherwise update element node logic.

Let’s look at the implementation of mountElement mountElement:

const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) = > {
    let el;
    let vnodeHook;
    const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode;
    {
        // Create a DOM element node
        el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is, props);

        if (shapeFlag & 8 /* TEXT_CHILDREN */) {
            // Handle the case where the child node is plain text
            hostSetElementText(el, vnode.children);
        } else if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
            // Handle the case where the child nodes are arrays
            mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type ! = ='foreignObject', slotScopeIds, optimized);
        }
        // Handle props, such as class, style, event, etc
        if (props) {
            for (const key in props) {
                if(key ! = ='value' && !isReservedProp(key)) {
                    hostPatchProp(el, key, null, props[key], isSVG, vnode.children, parentComponent, parentSuspense, unmountChildren); }}}// Handle CSS scopes
        setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent);
    }
    // Mount the created DOM element node to the container
    hostInsert(el, container, anchor);
};
Copy the code

As you can see, mount elements do five things: create DOM element nodes, work with text or array children, work with props, work with CSS scopes, and mount DOM elements to containers.

DOM elements are created using the hostCreateElement method, a platform-specific method that in a Web environment corresponds to:

function createElement(tag, isSVG, is) {
  isSVG ? document.createElementNS(svgNS, tag)
    : document.createElement(tag, is ? { is } : undefined)}Copy the code

It calls the underlying DOM API Document.createElement to create the element.

Similarly, if the child node is a text node, the hostSetElementText method is executed, which sets the text in the Web environment by setting the textContent attribute of the DOM element:

function setElementText(el, text) {
  el.textContent = text
}
Copy the code

Handle the props, handle the CSS scope, we won’t do the analysis right now.

To handle the case where the children are arrays, call the mountChildren method:

const mountChildren = (children, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, start = 0) = > {
    for (let i = start; i < children.length; i++) {
        // Preprocess the child node (optimize)
        const child = (children[i] = optimized
            ? cloneIfMounted(children[i])
            : normalizeVNode(children[i]));
        // Recursive patch mounts child
        patch(null, child, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized); }};Copy the code

The mount logic of the child node is also very simple, traversing children to obtain each child, and then recursively mount each child by performing the patch method.

It should be noted that recursive patch is a depth-first traversal of the tree.

After all the child nodes are processed, the DOM element node is finally mounted to the Container using the hostInsert method.