The author:Xu จ ุ ๊ บ, prohibit reprinting without authorization.

preface

The last article “Vue3 source code (a)” a brief introduction to the Vue3 source code structure, but also through the source code to learn the Vue3 foundation is also the core response. This time let’s move on to another core component, learning about Vue3 component initialization and its rendering process. If there are mistakes, omissions, but also hope to correct, supplement.


The body of the

Remember the initial Vue3 application mentioned in the last article?

createApp(App).mount('#app')

Mount (‘# App ‘) createApp(App) creates and returns a specific App instance using closures and Currification for different scenarios and platforms.

The mount method

Reviewing the previous article, we found that the source code defines the mount method in two main places:

  1. Runtime-dom/SRC /index.ts is rewritten for the browser Web platformmountmethods
  const { mount } = app
  app.mount = (containerOrSelector: Element | string): any= > {
    NormallizeContainer As the name implies, the mount argument may be a DOM object or a selector
    // Select the corresponding DOM if it is a selector
    const container = normalizeContainer(containerOrSelector)
    if(! container)return
    
    // App._component is the packaged and compiled app component we pass in with the rootComponent argument (Figure 1)
    const component = app._component
    
    // If we pass in a component that does not define render and does not have a template, use the original content in the DOM as the template
    if(! isFunction(component) && ! component.render && ! component.template) { component.template = container.innerHTML }// This will clear the original content in the DOM
    container.innerHTML = ' '
    
    // Execute the previously temporary base mount method
    const proxy = mount(container)
    container.removeAttribute('v-cloak')
    container.setAttribute('data-v-app'.' ')
    return proxy
  }
Copy the code

Figure 1:

Through the code and comments inside, you can divide the rewrite method into several steps: 1. 2. Determine the incoming root component App; 3. Perform the standard mount method.

  1. Runtime-core/SRC/apicreateapp. ts, which is a standard, cross-platform component in an app instancemountmethods
mount(rootContainer: HostElement, isHydrate? : boolean): any {// Whether the app has been mounted
   if(! isMounted) {Create VNode rootComponent = App component passed in createApp(App
     const vnode = createVNode(
       rootComponent as ConcreteComponent,
       rootProps
     )

     // App instance storage context, mainly app instance itself, various Settings, configuration items
     vnode.appContext = context

     if (isHydrate && hydrate) {
       // Server render related
       hydrate(vnode as VNode<Node, Element>, rootContainer as any)
     } else {
       // 2. render VNode
       // The "render" here is one that is created with one ensureRenderer mentioned in the previous article
       render(vnode, rootContainer)
     }
     isMounted = true
     
     // Store the DOM container
     app._container = rootContainer
     // for devtools and telemetry; (rootContaineras any).__vue_app__ = app
     // ...
     returnvnode.component! .proxy }else if (__DEV__) {
     // ...}},Copy the code

The standard mount method is as follows: 1. Create a VNode. 2. Render the VNode as a real DOM

summary

At this point, we know roughly what the mount method does.

  1. NormalizeContainer gets the DOM container
  2. CreateVNode, creates a VNode based on the incoming App component
  3. Render VNode and mount it to the DOM container
  4. Returns the proxy for vnode.ponent

Let’s move on to VNode.

Create & render vNodes

I believe that you are familiar with VNode, simply described through JavaScript objects abstract DOM, things. When asked about the benefits of the interview, you will probably mention these: 1. Don’t change the DOM as often, 2. 3. Performance advantages of VNode operating JS over DOM directly. However, after reading some articles recently, I believe that the third advantage is not absolute. For components with a large amount of data, such as Tree and Table, it takes a long time to iterate through the render sub-vNode, and DOM operation is still necessary in the end, and the page can even feel the lag.

Let’s go back to the example below

App.vue
<template>
  <HelloWorld msg=Vue 3.0 + Vite />
  <p>{{ showText }}</p>
</template>

HelloWorld.vue
<template>
  <div>{{ msg }}</div>
</template>
Copy the code

Create a VNode

In Vue3, there are many vNodes that represent different categories, such as the HelloWorld component VNode in the example above, and the common element VNode P.

In particular, let’s look at the createVNode method, which generates vNodes. The code is slightly longer, and the old method comments out the content that the process does not care about.

function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag: number = 0,
  dynamicProps: string[] | null = null,
  isBlockNode = false
) :VNode {
  if(! type || type === NULL_DYNAMIC_COMPONENT) { type = Comment }if (isVNode(type)) { // Clone the VNode, which is determined by the __v_isVNode attribute of type
    // createVNode receiving an existing vnode. This happens in cases like
    // <component :is="vnode"/>
    // #2078 make sure to merge refs during the clone instead of overwriting it
    const cloned = cloneVNode(type, props, true /* mergeRef: true */)
    if (children) {
      normalizeChildren(cloned, children)
    }
    return cloned
  }

  // class component normalization.
  if (isClassComponent(type)) { / / class components
    type = type.__vccOpts
  }

  // class & style normalization.
  if (props) {
    // ...
  }

  // Add an encoding identifier to the component type
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT // 1 dom element
    : __FEATURE_SUSPENSE__ && isSuspense(type)
      ? ShapeFlags.SUSPENSE // New components in Suspense Vue3
      : isTeleport(type)
        ? ShapeFlags.TELEPORT // 64 teleport is also new to VUe3
        : isObject(type)
          ? ShapeFlags.STATEFUL_COMPONENT // 4 Status components
          : isFunction(type)
            ? ShapeFlags.FUNCTIONAL_COMPONENT // 2 function components
            : 0
  // ...
  const vnode: VNode = {
    __v_isVNode: true,
    [ReactiveFlags.SKIP]: true,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    children: null.component: null,
    shapeFlag
    // ...
  }

  Type 8: text; 16: array; 32: slots; It also converts to the corresponding type. * The shapeFlag of VNode will be modified to use **/ for later mounting
  normalizeChildren(vnode, children)

  // normalize suspense children
  / /...
  
  return vnode
}

Copy the code

Take a look at the above code execution process through this example

  1. Check whether it is a VNode, Class component. If it is a Class component, perform the Class and style standardized conversion
  2. Determine the component type and calculate the identifier, resulting in 4
  3. Create a VNode
  4. Standard child node, where children is null when the App component is passed in
  5. Returns the VNode

Here we have the VNode created by the App component:

Rendering VNode

Then let’s look at render(vNode, rootContainer), how to render vNode.

Last time we looked at the Render method, baseCreateRenderer generates render for different platforms by passing in endererOptions for different platforms.

render
// runtime-core/src/renderer.ts
const render: RootRenderFunction = (vnode, container) = > {
  if (vnode == null) {
    if (container._vnode) {
      unmount(container._vnode, null.null.true)}}else {
    patch(container._vnode || null, vnode, container)
  }
  flushPostFlushCbs()
  // Store vNodes in the DOM container
  container._vnode = vnode
}

Copy the code

You can see that if the VNode passed in is empty and the current DOM container has a VNode, unmount the component to destroy it, otherwise patch the VNode passed in. Then we understand the implementation of patch.

patch
  const patch: PatchFn = (
    n1, // n1 represents the old node
    n2, // n2 represents the new node
    container,
    anchor = null,
    parentComponent = null,parentSuspense = null,isSVG = false,optimized = false
  ) = > {
    // If there is an old VNode and it is different, umount destroys the old node
    if(n1 && ! isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense,true)
      n1 = null
    }

    if (n2.patchFlag === PatchFlags.BAIL) {
      optimized = false
      n2.dynamicChildren = null
    }

    const { type, ref, shapeFlag } = n2
    // Select method by type
    switch (type) {
      case Text:
        / / text
        processText(n1, n2, container, anchor)
        break
      case Comment:
        / / comment
        processCommentNode(n1, n2, container, anchor)
        break
      case Static:
        / / static
        if (n1 == null) {
          mountStaticNode(n2, container, anchor, isSVG)
        } else if (__DEV__) {
          patchStaticNode(n1, n2, container, isSVG)
        }
        break
      case Fragment:
        // Fragmentation, which is Vue3's new support for multiple nodes
        processFragment(/ * *... * * /)
        break
      default:
        // If no type is specified, use shapeFlag
        if (shapeFlag & ShapeFlags.ELEMENT) {
          / / dom elements
          processElement(/ * *... * * /)}else if (shapeFlag & ShapeFlags.COMPONENT) {
          // This is where the component will go for its first rendering
          processComponent(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
          // There are two new components in Vue3
        } else if (shapeFlag & ShapeFlags.TELEPORT) {
          //
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
        }
    }

    // set ref
    if(ref ! =null && parentComponent) {
      setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2)
    }
  }
Copy the code

In fact, the most important logic of patch is to choose how to deal with components through vNode type and shapeFlag.

Since we are rendering for the first time, n1 is empty, and the App component created VNode with shapeFlags. STATEFUL_COMPONENT 4, we will go to the ShapeFlags.COMPONENT condition. Execute the processComponent method. So let’s take a look at this method.

processComponent
  const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) = > {
    if (n1 == null) {
    // If there is no old node
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { / / 512
      // If it is a keep-alive component; (parentComponent! .ctxas KeepAliveContext).activate(
          n2,
          container,
          anchor,
          isSVG,
          optimized
        )
      } else {
        // Execute the mount component
        mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      }
    } else {
      // If n1 and n2 are present, perform the update
      updateComponent(n1, n2, optimized)
    }
  }
Copy the code

The main logic of this method is to mount the component mountComponent, or to update the component with updateComponent.

Let’s look at the mountComponent to which the initial rendering is executed

mountComponent
  const mountComponent: MountComponentFn = (
    initialVNode,   // The initial VNode is the VNode generated by the App component
    container,  // #app Dom container
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized  
  ) = > {
    // Create a component instance
    const instance: ComponentInternalInstance = (initialVNode.component =    createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))


    // inject renderer internals for keepAlive
    if(isKeepAlive(initialVNode)) { ; (instance.ctxas KeepAliveContext).renderer = internals
    }
    
    // Set instance initialization props, slots, and Vue3's new Composition API
    setupComponent(instance)
    
    // ...

    // Effect is the side effect function mentioned in the previous article
    setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )
  }
Copy the code

The main logic for mounting a VNode component is createComponentInstance to create an instance of the component, setupComponent to set the component, and setupRenderEffect to perform a side effect rendering function.

CreateComponentInstance basically creates and returns instance instances, so let’s look at what instance looks like.


  const instance: ComponentInternalInstance = {
    uid: uid++,
    vnode,
    type,
    parent,
    appContext,
    root: null! .// to be immediately set
    next: null.subTree: null! .// will be set synchronously right after creation
    update: null! .// will be set synchronously right after creation
    render: null.proxy: null.withProxy: null.effects: null.provides: parent ? parent.provides : Object.create(appContext.provides),
    accessCache: null! , renderCache: [],// local resovled assets
    components: null.directives: null.// resolved props and emits options
    // 

    // emit
    emit: null as any, // to be set immediately
    emitted: null.// state
    ctx: EMPTY_OBJ,
    data: EMPTY_OBJ,
    props: EMPTY_OBJ,
    // ...

    // suspense related
    // ...

    // lifecycle hooks
    // The following are the attributes related to the component lifecycle
    isMounted: false.isUnmounted: false.isDeactivated: false.bc: null.// beforeCreate
    c: null.// created
    // ...
  }

Copy the code

The setupComponent method is also used to initialize the properties of instance, such as props, slots, and Vue3’s new setup function.

Because Vue3’s new Composition API and setup functions are involved, you can learn this separately

Once the instance is created and setup, the last step is to run the Render side effect function setupRenderEffect.

setupRenderEffect
  const setupRenderEffect: SetupRenderEffectFn = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) = > {
    // Create a reactive side effect render function
    instance.update = effect(function componentEffect() {
      if(! instance.isMounted) {let vnodeHook: VNodeHook | null | undefined
        const { el, props } = initialVNode 
        const { bm, m, parent } = instance // Life cycle, beforeMounted, mounted

        // bm lifecycle and hook execution
        if (bm) {
          invokeArrayFns(bm)
        }
        // ..
        
       // Render component generates subTree VNode
       const subTree = (instance.subTree = renderComponentRoot(instance))

        if (el && hydrateNode) {
          // ...
        } else {
          // Mount subTree into Dom container
          patch(
            null,
            subTree,
            container,
            anchor,
            instance,
            parentSuspense,
            isSVG
          )

          initialVNode.el = subTree.el
        }
        
        // The lifecycle is mounted hook
        if (m) {
          queuePostRenderEffect(m, parentSuspense)
        }
        // ...
        
        instance.isMounted = true
      } else {
        // updateComponent
        // This is triggered by mutation of component's own state (next: null)
        // OR parent calling processComponent (next: VNode)

      }
    },  prodEffectOptions)
  }
Copy the code

Review the content of the last article, the effect function must be familiar, run componentEffect trigger dependency collection, collect this effect function, when the component data changes, will re-execute the effect function componentEffect method.

ComponentEffect main logic is to generate a subTree VNode, and then mount the subTree.

renderComponentRoot
export function renderComponentRoot(
  instance: ComponentInternalInstance
) :VNode {
  const {
    type: Component,
    vnode,
    proxy,
    withProxy,
    props,
    propsOptions: [propsOptions],
    slots,
    attrs,
    emit,
    render,  // render is the.vue render function
    renderCache,
    data,
    setupState,
    ctx
  } = instance

  let result
  currentRenderingInstance = instance
  
  try {
    let fallthroughAttrs
    if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
      const proxyToUse = withProxy || proxy
      // In this example, Helloworld is looping, p tag VNoderesult = normalizeVNode( render! .call( proxyToUse, proxyToUse! , renderCache, props, setupState, data, ctx ) ) fallthroughAttrs = attrs }else {
      // functional
  } catch (err) {
    // ...
  }
  currentRenderingInstance = null

  return result
}
Copy the code

What is a subTree? For example, in the initial example, App component is initialVNode, subTree is VNode generated by the structure of App component template, children attribute is HelloWorld component VNode, and P tag VNode.

In the chidren of the App component initialVNode, the VNode generated according to the HelloWorld tag is initialVNode for the internal DOM structure of the HelloWorld component. The VNodes generated by the internal DOM structure are subtrees.

The following is the compiled render function for helloWorld.vue in this example

This is the App subTree

As you can see, children has Helloworld, p tag VNode.

After returning to the setupRenderEffect method and generating subTree, we will go back to our previous patch process to determine how to process the incoming VNodes. The cycle will continue until the patch real DOM elements, annotations and other VNodes.

I don’t know if you’ve noticed, but in the original example, the app. vue template doesn’t have a root node, which is a new feature in Vue3. In Vue2, you definitely need a div to enclose the HelloWorld, P tag.

So the APP component subTree in our example is resolved as a VNode with type Symbol(Fragment).

Go back to the Patch method and look at processFragment

  const processFragment = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) = > {
    // There is no root node
    const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(' '))!
    const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(' '))!
    // ...
    if (n1 == null) {
      hostInsert(fragmentStartAnchor, container, anchor)
      hostInsert(fragmentEndAnchor, container, anchor)
      // Children must be an array
      mountChildren(
        n2.children as VNodeArrayChildren,
        container,
        fragmentEndAnchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    } else{}}Copy the code

HostCreateText, hostInsert, and rendererOptions are props for creating render. For example hostCreateText is the document. The createTextNode hostInsert is the parent. The insertBefore (anchor child *, * * * | | null).

After processFragment determines the location, it executes mountChildren to process the Children VNode array.

mountChildren
const mountChildren: MountChildrenFn = (
    children,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized,
    start = 0
  ) = > {
    for (let i = start; i < children.length; i++) {
      const child = (children[i] = optimized
        ? cloneIfMounted(children[i] as VNode)
        : normalizeVNode(children[i]))
      // Patch each VNode
      patch(
        null,
        child,
        container, 
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    }
  }
Copy the code

MountChildren traverses children and patches each VNode to the current container.

Back to patch, let’s see how it works if it’s a DOM node VNode.

  const processElement = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) = > {
    if (n1 == null) {
      mountElement(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    } else {
      // }}Copy the code

Similar to the process of dealing with components, mount or update is determined by whether there are old nodes.

mountElement
  const mountElement = (
    vnode: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) = > {
    let el: RendererElement
    let vnodeHook: VNodeHook | undefined | null
    const {
      type,
      props,
      shapeFlag,
      transition,
      scopeId,
      patchFlag,
      dirs
    } = vnode
    // ...
      // Call the API passed in to create the DOM element
      el = vnode.el = hostCreateElement(
        vnode.type as string,
        isSVG,
        props && props.is
      )

      if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { / / 8
        // Create text if it is child node text
        hostSetElementText(el, vnode.children as string)
      } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { / / 16
        // If it is an array, go back to mountChildren to iterate over the patch child node
        // Notice that the container passed in here is already the newly created EL DOM element, thus creating the parent-child relationship
        mountChildren(
          vnode.children as VNodeArrayChildren,
          el,
          null, parentComponent, parentSuspense, isSVG && type ! = ='foreignObject', optimized || !! vnode.dynamicChildren ) }if (dirs) {
        // Call instruction related lifecycle processing
        invokeDirectiveHook(vnode, null, parentComponent, 'created')}// If there are PROPS for DOM, such as native class style, custom prop, etc
      if (props) {
        for (const key in props) {
          if(! isReservedProp(key)) { hostPatchProp( el, key,null,
              props[key],
              isSVG,
              vnode.children as VNode[],
              parentComponent,
              parentSuspense,
              unmountChildren
            )
          }
        }
        if ((vnodeHook = props.onVnodeBeforeMount)) {
          invokeVNodeHook(vnodeHook, parentComponent, vnode)
        }
      }
      // ...

    
    if (dirs) {
      invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')}/** Mount the created EL DOM to the contanier container ** First renders the container as the #app container, but then the corresponding parent DOM container **/
    hostInsert(el, container, anchor)
    
    // ...
  }
Copy the code

As you can see, the main logic for mounting a DOM node is to create the DOM by calling hostCreateElement, which is essentially the browser’s document.createElement. Then determine whether the child nodes are text or arrays. We then deal with the native or custom properties of the DOM. Finally, insert is called to mount to the DOM container.

Take the internal div of the HelloWorld component for example. Its children is just a piece of text we passed in through prop, so call hostSetElementText: el.textContent = *text* to insert the text.

One might wonder why div VNode’s shapeFlag is 9. Remember the normalizeChildren operation in the createVNode method? It changes the value of shapeFlag depending on whether children are of type array, text, or slot.

summary

Look at the code and see if the rendering process feels very convoluted, you can use the flow chart to understand it.

At the end

Thank you for reading. Recently, the big front end team of ZHicyun Health is participating in the nuggets popular team contest. If you think it’s good, then come and vote for us!

A total of 12 votes can be cast today, 4 votes can be cast on web, 4 votes can be cast on app. 4 votes can be cast on share. Thanks for your support, we will create more technical articles in 2021 ~~~

Your support is our biggest motivation ~