This article will analyze in detail how patch method converts VNode objects into real DOM

The patch method parses the VNode entry

RenderComponentRoot: Render ComponentRoot: Render ComponentRoot: Render ComponentRoot: Render ComponentRoot .call(…) Generates and returns a VNode object, and renderComponentRoot ultimately returns a VNode. When returning to setupRenderEffect function, renderComponentRoot function is executed when executing componentEffect method at first. See “Globally dependent activeEffect Collection “). Let’s parse the subsequent operations of the setupRenderEffect function

if (el && hydrateNode) {/ *... * /}
else {
    // ...
    patch(
      null,
      subTree,
      container,
      anchor,
      instance,
      parentSuspense,
      isSVG
    )
    // ...
    initialVNode.el = subTree.el
}
Copy the code

Container is the DOM object of the #app node. (For the container argument passed in by setupRenderEffect, see “mount”.)

const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    optimized = false
) = > {
    if(n1 && ! isSameVNodeType(n1, n2)) {/ *... N1 is null*/ during initial mount}
    if (n2.patchFlag === PatchFlags.BAIL/ * 2 * /) {/ *... * /}
    const { type, ref, shapeFlag } = n2
    switch (type) {
      case Text:
        processText(n1, n2, container, anchor)
        break
      case Fragment:
        processFragment(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT/ * 1 * /) {
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
        }
    }
}
Copy the code

The patch method parses the root VNode object

In this example, the vnode. type is Symbol(‘Fragment’)(see createBlock creating the root VNode object)

// processFragment method definition
const processFragment = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
) = > {
    const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(' '))!
    const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(' '))!

    let { patchFlag, dynamicChildren } = n2
    if (patchFlag > 0) {
      optimized = true
    }
    // ...
    if (n1 == null) {
      hostInsert(fragmentStartAnchor, container, anchor)
      hostInsert(fragmentEndAnchor, container, anchor)
      mountChildren(
        n2.children as VNodeArrayChildren,
        container,
        fragmentEndAnchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    } else{}}Copy the code

The app element inserts two empty text elements

The processFragment method first creates the EL and Anchor properties of the VNode, and when n1 does not exist (the old VNode object does not exist at the time of initial mount), HostCreateText (“) is the return value of hostCreateText(“). The baseCreateRenderer function defines a series of DOM manipulation methods, and the hostCreateText function is createText. CreateText (rendererOptions, rendererOptions, baseCreateRenderer, rendererOptions); NodeOps contains the nodeOps object that operates on DOM methods. See “entry function – App object generation” for details:

// Verify that the document attribute is present in the runtime environment
const doc = (typeof document! = ='undefined' ? document : null);
// Define the createText method
createText: text= > doc.createTextNode(text)
Copy the code

So createText calls createTextNode to create a text node, so n2’s EL and anchor properties are text nodes with empty contents, and then hostInsert is called. The hostInsert method corresponds to the INSERT method in the Options object, and the insert method corresponds to the nodeOps object as follows

insert: (child, parent, anchor) = > {
   parent.insertBefore(child, anchor || null)}Copy the code

So the hostInsert method inserts the fragmentStartAnchor and fragmentEndAnchor empty text nodes at the end of the Container node (DOM node of #app).

The mountChildren method parses VNode child objects

Call mountChildren to parse the children of VNode.

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(
        null,
        child,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    }
}
Copy the code

The mountChildren method loops through an array of children, continuing to call patch for each child VNode object.

ProcessText parses dynamic text objects

For the VNode object parsed above in this example, there are two elements in the children array. The first element is a Text object whose type is Symbol(Text), which is returned to the patch method. Switch-case calls the processText method according to the value of type

const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) = > {
    if (n1 == null) {
      hostInsert(
        (n2.el = hostCreateText(n2.children as string)),
        container,
        anchor
      )
    } else {
        // ...}}Copy the code

The first parameter of hostInsert is the return value of hostCreateText. The first parameter of hostCreateText is the return value of hostCreateText. The text node created by hostCreateText is n2. This is the children value of the first child object (in this case the string “test data “), and then the hostInsert method is called to insert the” test data “text node before the fragmentEndAnchor of the container(#app root DOM node).

test data

. “Test data” text is added to the page

ProcessElement Parses the HTML tag element object

After processing the first children element, return to the mountChildren method, continue to call the patch method to parse the VNode object of the second button element. At this time, type is the string “button”, return to the patch method, Enter the default branch according to type switch-case, and call processElement method to processElement VNode object according to shapeFlag value (9)

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

In processElement, the mountElement method is called based on n1== NULL

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
    if(! __DEV__ && vnode.el && hostCloneNode ! = =undefined && patchFlag === PatchFlags.HOISTED) {}
    else {
      el = vnode.el = hostCreateElement(
        vnode.type as string,
        isSVG,
        props && props.is
      )
      
      if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
          hostSetElementText(el, vnode.children as string)
      }
      // ... dirs
      if (props) {
        for (const key in props) {
          if(! isReservedProp(key)) { hostPatchProp(el, key,null, props[key], isSVG, vnode.children as VNode[], parentComponent, parentSuspense, unmountChildren)
          }
        }
        // ...
      }
      // scopeId
    }
    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      Object.defineProperty(el, '__vnode', {
        value: vnode,
        enumerable: false
      })
      Object.defineProperty(el, '__vueParentComponent', {
        value: parentComponent,
        enumerable: false})}if (dirs) {/ *... * /}
    constneedCallTransitionHooks = (! parentSuspense || (parentSuspense && ! parentSuspense.pendingBranch)) && transition && ! transition.persistedif (needCallTransitionHooks) {/ *... * /}
    hostInsert(el, container, anchor)
    / /... Asynchronous execution queue, v-show into this branch
}
Copy the code

CreateElement Creates an HTML tag DOM element

The mountElement function first calls hostCreateElement to create the element. HostCreateElement in options corresponds to createElement, and createElement in nodeOps corresponds to the following method

createElement: (tag, isSVG, is): Element= >
    isSVG
      ? doc.createElementNS(svgNS, tag)
      : doc.createElement(tag, is ? { is } : undefined)
Copy the code

Because isSVG is false, the document.createElement method is called, and props. Is is not present, so the argument is (tag, undefined), creating a button DOM node and assigning the EL attribute to the VNode.

SetElementText sets the element text content

Then, because shapeFlag=8, the hostSetElementText method is called. HostSetElementText corresponds to setElementText in options, and setElementText corresponds to the following method in nodeOps

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

Vnode. children = vnode.children = vnode.children = vnode.children = vnode.children = vnode.children = vnode.children Vnode. children is the child of the button child object, in this case the “modify data” string.

AddEventListener Adds an event listener for the element

If the props property of the VNode object exists, the hostPatchProp method will be called to add properties to the button element (isReservedProp method will be used to determine whether it is a specific property value, such as key, ref, and so on. In this case, onClick). The hostPatchProp function corresponds to the patchProp in the options object, and calls the rendererOptions object passed in by the baseCreateRenderer method. {patchProp, forcePatchProp} is also merged, see the concrete implementation of patchProp

/ / isOn the definition
const onRE = /^on[^a-z]/
export const isOn = (key: string) = > onRE.test(key)

/ / patchProp definition
export const patchProp: DOMRendererOptions['patchProp'] = (
  el,
  key,
  prevValue,
  nextValue,
  isSVG = false,
  prevChildren,
  parentComponent,
  parentSuspense,
  unmountChildren
) = > {
    switch (key) {
        default:
            if (isOn(key)) {
            // ignore v-model listeners
            if(! isModelListener(key)) { patchEvent(el, key, prevValue, nextValue, parentComponent) }// else-if
            break}}}Copy the code

In the method of patchProp, the switch-case branch is selected according to the value of key. In this case, key=onClick, enter the default branch, and call isOn method (starting with ON and the last character is not a lowercase letter A-Z) to determine whether it is an event listening attribute. In this case, true is returned. Then call the isModelListener method (the property name key starts with “onUpdate:”), which returns false in this case. So call the patchEvent function

export function patchEvent(el: Element & { _vei? : Record<string, Invoker |undefined> },
  rawName: string,
  prevValue: EventValue | null,
  nextValue: EventValue | null,
  instance: ComponentInternalInstance | null = null
) {
  // vei = vue event invokers
  const invokers = el._vei || (el._vei = {})
  const existingInvoker = invokers[rawName]
  if (nextValue && existingInvoker) {/ *... * /} 
  else {
    const [name, options] = parseName(rawName)
    if (nextValue) {
      // add
      const invoker = (invokers[rawName] = createInvoker(nextValue, instance))
      addEventListener(el, name, invoker, options)
    } else if (existingInvoker) {
      // remove ...}}}Copy the code

In patchEvent function, existingInvoker=undefined because el._vei is {} empty object, then call parseName method to resolve attribute name

const optionsModifierRE = / (? :Once|Passive|Capture)$/

function parseName(name: string) :string.EventListenerOptions | undefined] {
  let options: EventListenerOptions | undefined
  if (optionsModifierRE.test(name)) {/ *... * /}
  return [hyphenate(name.slice(2)), options]
}
Copy the code

ParseName method first determines whether the attribute name is Once | Passive | the Capture, otherwise it returns an array, the first element of the array is to obtain the attribute name of the second to last place, this example is the onClick, so name. Slice (2) = the click. The second element is options=undefined, and finally this method returns [click, undefined].

Return to the patchEvent method. Since nextValue exists, that is, the attribute value, createInvoker function is called

function createInvoker(
  initialValue: EventValue,
  instance: ComponentInternalInstance | null
) {
  const invoker: Invoker = (e: Event) = > {/ *... Define the invoker method */}
  invoker.value = initialValue
  invoker.attached = getNow()
  return invoker
}
Copy the code

The createInvoker function internally defines the invoker method, setting invoker.value to the props property value, in this case the modifyMessage method, and finally returning the Invoker function. Go back to the patchEvent method and continue calling the addEventListener function

export function addEventListener(el: Element, event: string, handler: EventListener, options? : EventListenerOptions) {
  el.addEventListener(event, handler, options)
}
Copy the code

The addEventListener method adds an event(click event) listener to the el(button element created). The event callback is invoker. Then go back to the mountElement method and continue calling hostInsert to insert el(button element) before the Anchor (fragmentEndAnchor empty text node) text element of the Container (DOM element of #app).

Returning to the mountChildren method, taking this template as an example, children under the root VNode have all cyclically parsed and generated DOM elements.

test data

Button has a click event on it

At this point, the Patch method has completed parsing the VNode object to generate DOM elements. The next key point is the button event listener. Click the button to trigger the event listener callback, execute the modifyMessage method, change the message.value value, trigger the set hook function, and regenerate the VNode.

conclusion

The patch method is used to parse the root VNode object, add two empty text nodes to the #app element, and then loop through the child elements (vnode.children array). Insert in sequence before the second empty text DOM node. Then for the HTML tag element (as in this case, the Button tag), create the button element, add the text content of the element (the button content), and then add a click event listener on the button element. It will then parse how the callback function is executed when the button is clicked, how the data is modified, and how the DOM is triggered to update.