In Vue.js, components are a very important concept, and the entire application page is realized through component rendering

Think about:

  • How does the inner workings of these components work when we write them?
  • What is the transition from writing a component to the actual DOM

First, a component is an abstraction of a DOM tree. We write a component node on the page:

<hello-world></hello-world>
Copy the code

This code does not render a

tag on the page, but what it renders depends on how you write the template for the HelloWorld component.

For example, the template definition inside the HelloWorld component looks like this:

<template>
   <div>
      <p>Hello World</p>
   </div>
</template>
Copy the code

As you can see, the template ends up rendering a div on the page with a P tag inside that displays the Hello World text.

So, in terms of representation, the component template determines the DOM tag generated by the component, while inside vue.js, a component that wants to actually render the DOM needs to go through the steps of “create vNode-render vNode-generate DOM” :

Next, we’ll take a step-by-step look at how components in Vue.js 3.0 are rendered, starting at the entry point of the application.

Application initialization

A component can be created by “template plus object description”. How is the component called and initialized after it is created? Since the entire tree of components starts rendering with the root component, to find the root component’s rendering entry, we need to start analyzing the application’s initialization process:

First, let’s compare the code for vue2.x and Vue3.0 initialization applications

// Import Vue from 'Vue' import App from './App' const App = new Vue({render: render) h => h(App) }) app.$mount('#app')Copy the code
// In vue.js 3.0, Import {createApp} from 'vue' import App from './ App 'const App = createApp(App) app.mount('# App ')Copy the code

As you can see, vue.js 3.0 initializes the application in the same way as vue.js 2.x, essentially mounting the App component to the DOM node with the ID App.

Vue. Js 3.0, however, also imports a createApp, which is a function exposed by vue. js.

const createApp = ((... Args) => {const app = ensureRenderer().createApp(... Args) const {mount} = app. Mount = (containerOrSelector) => {//... } return app })Copy the code

As you can see from the code, createApp does two main things: create an app object and override the app.mount method. Now, let’s look at them in detail.

1. Create app objects

First we create an app object with ensureRenderer().createApp() : ensureRenderer();

const app = ensureRenderer().createApp(... args)Copy the code

One that is ensureRenderer() is used to create a renderer object with the same internal code:

Const rendererOptions = {patchProp,... NodeOps} let renderer // delay the creation of renderer, when the user only relies on reactive packages, Can be removed through the tree - shaking core rendering logic related code function ensureRenderer () {return the renderer | | (= the renderer createRenderer(rendererOptions)) } function createRenderer(options) { return baseCreateRenderer(options) } function BaseCreateRenderer (options) {function render(vnode, container) {return {render, createApp: CreateAppAPI (render)}} function createAppAPI(render) {// createApp Prop Return function createApp(rootComponent, rootProps = null) {const app = {_component: rootComponent, _props: RootProps, mount(rootContainer) {// createVNode of rootComponent const vnode = createVNode(rootComponent, RootProps) // Vnode render(vnode, rootContainer) app._container = rootContainer return vnode.component.proxy } } return app } }Copy the code

EnsureRenderer () is used to delay the creation of a renderer. The advantage of this is that the renderer cannot be created if the user relies only on responsive packages, so the code associated with the core rendering logic can be removed tree-shaking

The concept of a renderer, which is cross-platform ready, is covered here, and can simply be understood as a JavaScript object that contains the core logic of platform rendering.

In Vue. Js 3.0, create a renderer using createRenderer. Inside the renderer will be a createApp method that executes the function returned by the createAppApi method. Taking rootComponent and rootProps, when we execute createApp(App) at the application level, we pass the App component object as the rootComponent to rootComponent. This creates an app object inside createApp that provides the mount method used to mount the component.

In the whole process of app object creation, vue.js makes good use of closure and function currification techniques to achieve parameter reservation. For example, app.mount does not require the render to be passed in because the render parameter is already preserved when createAppApi is executed.

2. Override the app.mount method

From the previous analysis, we know that the app object returned by createApp already has a mount method, but in the entry function, the logic is to rewrite the app.mount method.

Think about a problem?

Why override this method instead of putting the logic inside the app object's mount method?Copy the code

This is because vue.js isn’t just for Web platforms, it aims to support cross-platform rendering, and the app.mount method inside createApp is a standard cross-platform component rendering process

Mount (rootContainer){// Create root vnode const vnode = createVNode(rootComponent, // Render vnode render(vnode, rootContainer) app._container = rootContainer return vnode.component.proxy}Copy the code

The standard cross-platform rendering process is to create a VNode and then render a VNode. The rootContainer parameter can also be a different type of value, for example, a DOM object on the Web platform and a different type of value on other platforms, such as Weex and applets. Therefore, the code should not contain any platform-specific logic, which means that the execution logic of the code is platform-independent. So we need to rewrite this method externally to improve the rendering logic of the Web platform

Next, let’s look at what the app.mount rewrite does:

App. mount = (containerOrSelector) => {// Standardized container const container = normalizeContainer(containerOrSelector) if (! Container) return const component = app._component // If the component object does not define the render function and template, take the innerHTML of the container as the component template content. isFunction(component) && ! component.render && ! Component.template) {component.template = container.innerhtml} // Empty the contents of the container before mounting it mount(container) }Copy the code

The first step is to normalize the container with normalizeContainer (you can pass in a string selector or A DOM object, but if it’s a string selector, you need to turn it into a DOM object as the ultimate mounted container), and then do an if judgment, If the component object does not define the Render function and template template, the innerHTML of the container is taken as the component template content. It then empties the contents of the container before mounting it, and finally calls app.mount to follow the standard component rendering process.

In this case, the rewrite logic is Web platform-specific, so it is implemented externally. In addition, the goal is to give the user more flexibility in using the API and also to be compatible with the Vue. Js 2.x writing style, such as app.mount’s first argument supports both selector string and DOM object types.

Since app.mount is the start of the component rendering process, let’s focus on two things the core rendering process does: create a VNode and render a VNode.

Core rendering process: Create vNode and render Vndoe

1. Create vnode

A VNode is essentially a javaScript object used to describe the DOM. It can describe different types of nodes in vue.js, such as common element nodes, component nodes, etc.

What are ordinary element nodes? For example, in HTML we use the

<button class="btn" style="width:100px; height:50px;" >click me</button>Copy the code

const vnode = {
    type: 'button',
    props: {
        'class': 'btn',
        style:{
             width: '100px',
             height: '50px'
        }
    },
    children: 'click me'
}
Copy the code

The type attribute represents the tag type of the DOM, the props attribute represents some additional information about the DOM, such as style and class, and the children attribute represents a child node of the DOM. It can also be a VNode array, but vNodes can represent simple text as strings.

What is a component node? In fact, vNodes can be used to describe a real DOM like the ones above, and can also be used to describe components.

We now introduce a component tag < custom-Component > in the template:

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

The

component tag can be represented by vnode as follows:

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

The component vNode is actually 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.

In addition to the two vnode types above, there are plain text vNodes, comment vnodes, etc. We only need to study component vNodes and plain element vNodes, so I won’t go into the rest here

In addition, vue. js 3.0 also makes more detailed classification for Vnode and type, including Suspense, Teleport, etc., and codes vnode type information so that corresponding processing logic can be executed according to different types in the later patch stage:

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
Copy the code

Now that you know what a VNode is, you may be wondering, what are the advantages of a VNode? Why design a data structure like VNode?

The first is abstraction. With the introduction of VNode, the rendering process can be abstracted, thus improving the abstraction ability of components.

Secondly, cross-platform, because the patch vnode process of different platforms have their own implementation, based on VNode to do server rendering, Weex platform, small program platform rendering have become a lot easier.

However, it is important to note that using Vnode does not mean different DOM manipulation. Many students will mistakenly assume that Vnode performance is better than manual manipulation of the native DOM, which is not necessarily the case.

Because, first of all, this MVVM framework based on vNode implementation will take some JavaScript time to render components during each render to VNode process, especially large components, such as a 1000*10 Table component, The process of render to vnode will traverse 1000 * 10 times to create the internal cell vnode, which will take a long time. In addition, the process of patch vnode will also take a certain amount of time. When we go to update components, users will feel obvious lag. While the Diff algorithm is good enough to minimize DOM manipulation, ultimately DOM manipulation is unavoidable, so performance is not an advantage of VNode.

So how are these VNodes created inside vue.js?

Review the implementation of the app.mount function, which internally creates the vnode of the root component via the createVNode function:

 const vnode = createVNode(rootComponent, rootProps)
Copy the code

Let’s look at the rough implementation of the createVNode function:

Function createVNode(type, props = null, children = null) {if (props) {// 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 properties} NormalizeChildren (vnode, children) return vnode}Copy the code

CreateVNode does a simple thing: normalize props, code vnode types, createVNode objects, and standardize children. With the VNode created, we now need to render it to the page

2. Render the VNode

Review the implementation of the app.mount function, internally rendering the created vNode by executing this code:

render(vnode, rootContainer) const render = (vnode, Container) => {if (vnode == null) {// Destroy components if (container._vnode) {unmount(container._vnode, null, null, True)}} else {/ / create a patch or update components (container. _vnode | | null, vnode, container)} / / cache the vnode nodes, 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.

Now let’s look at the implementation of the patch function involved in the vNode rendering code above:

const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, Optimized = false) => {// If there are old nodes and the new node type is different, destroy the old node if (n1 &&! isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense, True) n1 = null} const {type, shapeFlag} = n2 Switch (type) {case Text: Break case Static: // Break case Fragment: // Break case default: If (shapeFlag & 1 /* ELEMENT */) {// Handle the normal DOM ELEMENT processElement(n1, n2, container, anchor, parentComponent, ParentSuspense, isSVG, Optimized)} else if (shapeFlag & 6 /* COMPONENT */) { container, anchor, parentComponent, parentSuspense, isSVG, Optimized)} else if (shapeflag&64 /* TELEPORT */) {// TELEPORT} else if (shapeflag&64 /* SUSPENSE */) {//  SUSPENSE } } }Copy the code

This function has two functions, one is to mount the DOM according to the VNode, and the other is to update the DOM according to the old and new VNodes. For the first rendering, we analyze the creation process, and the update process is followed by a separate analysis.

In the process of creation, patch function accepts multiple functions, and we only focus on the first three at present:

  • The first argument, n1, represents the old VNDOE. When n1 is null, it indicates a mount process.
  • The second parameter, n2, represents the new VNode, and different processing logic is later performed depending on the vNode type
  • 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 and here on the rendering logic of nodes: processing of components and processing of ordinary DOM elements.

Let’s take a look at the processing of components. Since the initial render is the App component, which is a component VNode, let’s take a look at the processing logic of the component. The first is the implementation of the processComponent function that handles the component:

const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, Optimized) => {if (n1 == null) {// mountComponent mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, Optimized)} else {// updateComponent updateComponent(n1, n2, parentComponent, 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) => {// createComponentInstance const instance = (initialVNode.component = createComponentInstance(initialVNode, ParentComponent, parentSuspense) // setupComponent(instance) // Set and run the render function setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) }Copy the code

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

The first step is to create component instances. Vue.js 3.0 does not instantiate components in a drum like vue2.x, but internally creates the currently rendered component instance through object methods.

Second, set the component instance. Instance retains a lot of component-related data and maintains the context of the component, including initialization of props, slots, and other instance properties.

Finally, setupRenderEffect is a rendering function that allows side effects. Let’s focus on the implementation of this function

const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, Optimized) => {// Create a rendering function instance.update = effect(function componentEffect() {if (! Instance. isMounted) {// Render component generates subTree vNode const subTree = (instance.subTree = renderComponentRoot(instance)) // Render component generates subTree vNode const subTree = (instance Patch (NULL, subTree, container, anchor, instance, parentSuspense, IsMounted = true} else {// Update component}}, prodEffectOptions) }Copy the code

This function uses the effect function of the reactive library to create a side effect rendering function componentEffect. The side effect can be simply understood as: When the data of the component changes, the internal rendering function componentEffect wrapped by the Effect function will be executed again, so as to achieve the purpose of re-rendering the component.

The render function also determines whether this is an initial render or a component update. We will only analyze the initial rendering flow here.

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

First, the render component generates the subTree, which is also a VNode object. It is important not to confuse subTree with initialVNode (in vue.js 3.0 they are well distinguished by their names, whereas in vue.js 2.x they are named _vnode and $vnode respectively). For example, introducing the Hello component in the parent App:

<template>
  <div class="app">
    <p>This is an app.</p>
    <hello></hello>
  </div>
</template>
Copy the code

Inside the Hello component is the

tag wrapped around a

tag:

<template> <div class="hello"> <p>Hello, Vue 3.0! </p> </div> </template>Copy the code

In App component, the vNode generated by < Hello > node rendering corresponds to the initialVNode of Hello component, which can also be called “component vNode” for easy memory. The vNode corresponding to the entire DOM node inside the Hello component executes renderComponentRoot rendering to generate the corresponding subTree, which can be called “sub-tree VNode”.

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.

The patch function will continue to judge the type of the subtree vnode. For the above example, the root node of the App component is the

tag, so the corresponding subtree vnode is also a common element vnode. So let’s look at the process for normal DOM elements.

Let’s first look at the implementation of the processElement function that handles ordinary DOM elements:

const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, Optimized) = > {isSVG = isSVG | | n2 type = = = 'SVG' if (n1 = = null) {/ / mount element node mountElement (n2, container, anchor, ParentComponent, parentSuspense, isSVG, Optimized)} else {// Update element node patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized) } }Copy the code

The logic of this function is simple: if n1 is null, mount the element node logic, otherwise update the element node logic.

Let’s look at the implementation of mountElement mountElement:

const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => { let el const { type, props, ShapeFlag} = vnode el = vnode.el = hostCreateElement(vnode.type, isSVG, Props && props. Is) if (props) {// For (const key in props) {if (! isReservedProp(key)) { hostPatchProp(el, key, null, props[key], IsSVG)}}} if (shapeflag&8 /* TEXT_CHILDREN */) { Vnode.children)} else if (shapeFlag & 16 /* ARRAY_CHILDREN */) {mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type ! == 'foreignObject', optimized || !! // Mount the created DOM element node to the container hostInsert(el, container, anchor)}Copy the code

As you can see, the mount element function does four things: create DOM element nodes, handle props, handle children, and mount DOM elements to containers.

The first step is to create a DOM element node using the hostCreateElement method, which is a platform-specific method. Let’s see how it is defined in a Web environment:

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, so vue.js essentially doesn’t manipulate the DOM. It just doesn’t want the user to touch the DOM directly. There’s nothing magical about vue.js.

In addition, on other platforms such as Weex, the hostCreateElement method no longer manipulates the DOM, but rather platform-specific apis that are passed in as arguments during the renderer creation phase.

After the DOM node is created, the next thing to do is to check if there are props, add class, style, event and other properties to the DOM node, and perform related processing. All these logic is done inside the hostPatchProp function, which will not be discussed here.

Next comes the child node processing. We know that DOM is a tree, and vNode is also a tree, and it maps to the DOM structure one by one.

If the child node is plain text, 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

If the child nodes are arrays, the mountChildren method is executed:

const mountChildren = (children, container, anchor, parentComponent, parentSuspense, isSVG, optimized, start = 0) => { for (let i = start; i < children.length; I ++) {// Prefetch child const child = (children[I] = optimized? cloneIfMounted(children[i]) : NormalizeVNode (children[I]) // Mounting Child Patch (null, child, container, Anchor, parentComponent, parentSuspense, isSVG, 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. Note that there is some preprocessing of the child (more on that in the section on compiler optimization).

As you can see, the second argument to the mountChildren function is container, and the second argument we pass to the mountChildren method is the DOM node created in the mountElement. This nicely establishes the parent-child relationship.

In addition, we can construct a complete DOM tree and complete the rendering of components through recursive patch, which 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, which is defined in the Web environment as follows:

function insert(child, parent, anchor) {
  if (anchor) {
    parent.insertBefore(child, anchor)
  }
  else {
    parent.appendChild(child)
  }
}
Copy the code

Parent. insertBefore if there is a reference element anchor, otherwise parent.appendChild adds the child to parent and completes the node mount.

Because insert is executed after the child nodes are processed, the mount sequence is child first, then parent, and finally to the outermost container.

Tips: Nested components

MountChildren recursively executes patch instead of mountElement because child nodes may have other types of VNodes, such as component VNodes.

Nested component scenarios are common in real-world development scenarios, such as the App and Hello component examples. The component VNode mainly maintains the component definition object and various props on the component. The component itself is an abstract node, and its rendering is actually completed by executing the render function defined by the component to render the subtree vNode, and then patch. In this recursive manner, the entire tree of components can be rendered, no matter how deep they are nested.

Finally, let’s use a diagram to get a better sense of the component rendering process: