Render to VNode: Render to VNode: render to VNode As we know, Vue has the ability to cross multiple terminals, provided that VNode (JavaScript objects) is used. If you have “compile 🔨”, you can do whatever you want. In this article, I’ll look at VNode’s DOM generation process for the Web platform. By the end of this article, you will have learned:

  1. Ordinary nodalpatchThe process of rendering;
  2. Component nodepatchThe process of rendering;
  3. A little trick (hidden in the article 🤭🤭🤭);

Patch of a common node

Normal nodes analyze the rendering process using a div 🌰 :

<div id="app">
</div>
<script>
    new Vue({
        el: '#app'.name: 'App',

        render (h) {
            return h('div', {
                id: 'foo'
            }, 'Hello, patch')}})</script>
Copy the code

🌰 after _render, get VNode as shown in the following figure:

Then we call _update to generate the DOM:

updateComponent = function () {

    // vm._render() generates the virtual node
    // Call vm._update to update the DOM
    vm._update(vm._render(), hydrating);
};
Copy the code

Vm._update is defined in init lifecycleMixin (this part of the code is in SRC \core\instance\lifecycle. Js) :

Vue.prototype._update = function (vnode: VNode, hydrating? : boolean) {
    const vm: Component = this

    // Update parameters
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    activeInstance = vm

    // Cache virtual nodes in _vnode
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    // The first render did not compare VNode, null here
    if(! prevVnode) {// initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)}else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }
Copy the code

The main logic is vm.__patch__, which is the core of being able to accommodate multiple applications! A global search for vue.prototype. __patch__ shows several definitions:

This article only looks at the logic on the Web side:

// src\platforms\web\runtime\index.js
// This function is null to determine whether it is in the browser environment. Server rendering does not need to render to the DOM, so it is null
Vue.prototype.__patch__ = inBrowser ? patch : noop

// src\platforms\web\runtime\patch.js
/ * * *@param {Object} -nodeOps encapsulates a set of DOM manipulation methods *@param {Object} -modules defines the implementation of some module hook functions */
export const patch: Function = createPatchFunction({ nodeOps, modules })

// src\core\vdom\patch.js
export function createPatchFunction (backend) {
 
  / /... 😎 Wear sunglasses and don't look at these hundreds of lines of code

  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // ... 
    return vnode.elm
  }
}
Copy the code

With hundreds of lines of code removed, we can clearly understand the patch acquisition process:

  • Clients are distinguished by directories.webDOMOperations and hooks are all locatedsrc\platforms\web.weexThe render functions and hooks are all located insrc\platforms\weex.
  • By calling atsrc\core\vdom\patch.jsUnder thecreatePatchFunctiongeneratepatchFunction. Our application will definitely call the render function repeatedly, throughThe technique of curryingThe difference of the platform is smoothed once, and then called each timepatchThere is no need to iterate over and over to get manipulation functions for the platform (❗❗❗ tip).

After retrieving the patch function, let’s look at the rendering process:

// This is null for the first rendering, and is not null when the data changes and is rerendered
if(! prevVnode) {// initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)}else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode)
}
Copy the code

Vm. __Patch__ is the return value of createPatchFunction:

/** * render function *@param {VNode} OldVnode oldVnode *@param {VNode} Vnode Indicates the current Vnode node@param {Boolean} Hydrating Specifies whether to render on the server@param {Boolean} RemoveOnly this is used by transition-group components. */ is not involved
return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // No vnode is generated after _render, and the old node, if any, is destroyed
    if (isUndef(vnode)) {
        if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
        return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    // There is no oldNode for the first render
    if (isUndef(oldVnode)) {
        // empty mount (likely as component), create new root element
        isInitialPatch = true
        createElm(vnode, insertedVnodeQueue)
    } else {
        const isRealElement = isDef(oldVnode.nodeType)

        // The component was updated with oldNode, so perform sameVnode for comparison
        if(! isRealElement && sameVnode(oldVnode, vnode)) {// patch existing root node
            patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
        } else {
            if (isRealElement) {
                // mounting to a real element
                // check if this is server-rendered content and if we can perform
                // a successful hydration.
                if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
                    oldVnode.removeAttribute(SSR_ATTR)
                    hydrating = true
                }
                / /... Eliminating server-side rendering logic
                // either not server-rendered, or hydration failed.
                // create an empty node and replace it
                // Convert the real DOM to VNode
                // 🌰 is the DOM id = app
                oldVnode = emptyNodeAt(oldVnode)
            }

            // replacing existing element
            // Mount the node, 🌰 is the id = app DOM
            const oldElm = oldVnode.elm
            // Mount the parent node of the node, with body in 🌰
            const parentElm = nodeOps.parentNode(oldElm)

            // Create a new node
            createElm(
                vnode,
                insertedVnodeQueue,
                // extremely rare edge case: do not insert if old element is in a
                // leaving transition. Only happens when combining transition +
                // keep-alive + HOCs. (#4590)
                oldElm._leaveCb ? null : parentElm,
                // 🌰 returns a newline
                nodeOps.nextSibling(oldElm)
            )

            // ...

            // Destroy the old node. In 🌰 parentElm is the body element
            if (isDef(parentElm)) {
                // oldVnode is a div with id = app
                removeVnodes(parentElm, [oldVnode], 0.0)}else if (isDef(oldVnode.tag)) {
                invokeDestroyHook(oldVnode)
            }
        }
    }

    // Perform the insert hook
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
}
Copy the code

CreateElement creates a new DOM via VNode and inserts it into its parent node. Is the main process of rendering:

// Create a real DOM from the virtual node and insert it into its parent node
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
    // ...
     
    // 🌰 {id: 'foo'}
    const data = vnode.data
    // 🌰 is a text node
    const children = vnode.children
    // in 🌰 is div
    const tag = vnode.tag

    / /... Omit to determine whether the label is valid

    // Call a platform DOM operation to create a placeholder element
    vnode.elm = vnode.ns
       ? nodeOps.createElementNS(vnode.ns, tag)
       : nodeOps.createElement(tag, vnode)
       // Scoped style processing, not involved here
       setScope(vnode)

        /* istanbul ignore if */
        if (__WEEX__) {
            / /... Omit the WEEX code
        } else {
            // Create a child node
            createChildren(vnode, children, insertedVnodeQueue)
            if (isDef(data)) {

                // Execute all create hooks and push vnode into insertedVnodeQueue
                invokeCreateHooks(vnode, insertedVnodeQueue)
            }

            // Insert the DOM into the parent node. Since this is a deep recursive call, insert the DOM first after the child
            insert(parentElm, vnode.elm, refElm)
        }

        if(process.env.NODE_ENV ! = ='production' && data && data.pre) {
            creatingElmInVPre--
        }
     // Comment node
    } else if (isTrue(vnode.isComment)) {
        vnode.elm = nodeOps.createComment(vnode.text)
        insert(parentElm, vnode.elm, refElm)
    } else {
        // Text node
        vnode.elm = nodeOps.createTextNode(vnode.text)
        insert(parentElm, vnode.elm, refElm)
    }
}
Copy the code

Create a placeholder element with nodeops.createElement:

export function createElement (tagName: string, vnode: VNode) :Element {
  // create a div element in 🌰
  const elm = document.createElement(tagName)
  // return div instead of select
  if(tagName ! = ='select') {
    return elm
  }
  // false or null will remove the attribute but undefined will not
  if(vnode.data && vnode.data.attrs && vnode.data.attrs.multiple ! = =undefined) {
    elm.setAttribute('multiple'.'multiple')}return elm
}
Copy the code

The createChildren depth iterates recursively to create child nodes:

function createChildren (vnode, children, insertedVnodeQueue) {
    if (Array.isArray(children)) {
        // Check for duplicate keys, if any
        if(process.env.NODE_ENV ! = ='production') {
            checkDuplicateKeys(children)
        }

        // Iterate over the child virtual nodes, recursively calling createElm
        for (let i = 0; i < children.length; ++i) {

            // Vnode. elm will be used as a DOM node placeholder for the parent container during traversal
            createElm(children[i], insertedVnodeQueue, vnode.elm, null.true, children, i)
        }
    } else if (isPrimitive(vnode.text)) {
        nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
    }
}
Copy the code

InvokeCreateHooks execute all createXX hooks:

// Execute all create hooks and push vnode into insertedVnodeQueue
invokeCreateHooks(vnode, insertedVnodeQueue)

function invokeCreateHooks (vnode, insertedVnodeQueue) {
    // This function is defined under SRC \platforms\web\runtime\modules
    for (let i = 0; i < cbs.create.length; ++i) {
        cbs.create[i](emptyNode, vnode "i")
    }
    i = vnode.data.hook // Reuse variable
    if (isDef(i)) {
        // Create and insert hooks
        if (isDef(i.create)) i.create(emptyNode, vnode)
        if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
    }
}
Copy the code

Finally, insert into the DOM by calling insert:

// Insert the DOM into the parent node. Since this is a deep recursive call, insert the DOM first after the child
insert(parentElm, vnode.elm, refElm)

/** * dom insert function *@param {*} parent- Parent node *@param {*} elm- Child node *@param {*} ref* /
function insert (parent, elm, ref) {
    if (isDef(parent)) {
        if (isDef(ref)) {
            if (ref.parentNode === parent) {
                // https://developer.mozilla.org/zh-CN/docs/Web/API/Node/insertBefore
                nodeOps.insertBefore(parent, elm, ref)
            }
        } else {
            // https://developer.mozilla.org/zh-CN/docs/Web/API/Node/appendChild
            nodeOps.appendChild(parent, elm)
        }
    }
}
Copy the code

After performing the insert, the DOM of the page changes once, as shown in the figure below in 🌰 :

Patch removes the placeholder node and performs the destroy and insert hook functions:

// Destroy the old node. In 🌰 parentElm is the body element
if (isDef(parentElm)) {
    // oldVnode is a div with id = app
    removeVnodes(parentElm, [oldVnode], 0.0)}else if (isDef(oldVnode.tag)) {
    invokeDestroyHook(oldVnode)
}
Copy the code

summary

At this point, the initial rendering process of ordinary nodes is analyzed. The patch process completes rendering by creating nodes, inserting nodes recursively, and finally destroying placeholder nodes. The actions in the procedure are all calls to the DOM native API. Component nodes are also placeholder nodes. Let’s analyze the rendering process of components.

Components of the patch

🌰 in this section transfers the above logic to the Child component:

<div id="app">
</div>
<script>
    const Child = {
        render(h) {
            return h('div', {
                id: 'foo'.staticStyle: {
                    color: 'red',},style: [{
                    fontWeight: 600}},'component patch')}}new Vue({
        el: '#app'.components: {
            Child
        },

        name: 'App'.render(h) {
            return h(Child)
        }
    })
</script>
Copy the code

Component rendering returns true when executing the following logic on createElm:

if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
}

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
        / / keep alive - components
        const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
        // There is an installComponentHooks logic in the generate VNode section that generates components
        if (isDef(i = i.hook) && isDef(i = i.init)) {
            i(vnode, false /* hydrating */)}// after calling the init hook, if the vnode is a child component
        // it should've created a child instance and mounted it. the child
        // component also has set the placeholder vnode's elm.
        // in that case we can just return the element and be done.
        if (isDef(vnode.componentInstance)) {
            initComponent(vnode, insertedVnodeQueue)
            insert(parentElm, vnode.elm, refElm)
            if (isTrue(isReactivated)) {
                reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
            }
            return true}}}Copy the code

I (vnode, false /* hydrating */)

const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ? boolean {// The keepalive logic is not concerned in this article
    if( vnode.componentInstance && ! vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) {// kept-alive components, treat as a patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      // Create a Vue instance
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      // Call the $mount method to mount the child component
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },
  // ...
}

/** * Create virtual node component instances *@param {*} vnode 
 * @param {*} parent 
 */
export function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
) :Component {
  // Construct the component parameters
  const options: InternalComponentOptions = {
    _isComponent: true._parentVnode: vnode,
    parent
  }
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  // Subcomponent constructor
  return new vnode.componentOptions.Ctor(options)
}
Copy the code

New to vnode.com ponentOptions. Ctor (options) will be performed to _init logic, but the different is the following logic will be performed:

if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options);
}

// src\core\instance\init.js
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {

  $options = object.create (sub.options) $options = object.create (sub.options)
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.

  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}
Copy the code

The initInternalComponent is also simple, with parameters merged into $options. $mount(hydrating? Vnode.elm: undefined, hydrating) mount logic, where “hydrating” is false because it is not rendered on the server, equivalent to child. mount(undefined, false) Run mount -> mountComponent -> updateComponent -> vm._render-> vm._update. When you go to vm._update, the process is the same as the normal node process.

Another difference in this section is the addition of staticStyle and style to the Child component. We know that these styles will eventually render as strings on the DOM. The render results for 🌰 are as follows:

<div style="color: red; font-weight: 600;">component patch</div>
Copy the code

When you execute the invokeCreateHooks, the updateStyle hook is executed:

function updateStyle (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  const data = vnode.data
  const oldData = oldVnode.data

  if (isUndef(data.staticStyle) && isUndef(data.style) &&
    isUndef(oldData.staticStyle) && isUndef(oldData.style)
  ) {
    return
  }

  let cur, name
  const el: any = vnode.elm
    
  When first rendered in 🌰, these oldXX are empty objects
  const oldStaticStyle: any = oldData.staticStyle
  const oldStyleBinding: any = oldData.normalizedStyle || oldData.style || {}

  // If the static style exists, then the style binding has been merged into it when normalizeStyleData is executed
  const oldStyle = oldStaticStyle || oldStyleBinding

  // Formalize the dynamic style and convert it to {key: value}
  const style = normalizeStyleBinding(vnode.data.style) || {}

  // store normalized style under a different key for next diff
  // make sure to clone it if it's reactive, since the user likely wants
  // to mutate it.
  // Cache the style result to data.normalizedStyle
  vnode.data.normalizedStyle = isDef(style.__ob__)
    ? extend({}, style)
    : style

  // Concatenate all style properties, including staticStyle and style
  const newStyle = getStyle(vnode, true)

  for (name in oldStyle) {
    if (isUndef(newStyle[name])) {
      setProp(el, name, ' ')}}// After smoothing out the data structure differences, iterate over the set to the DOM
  for (name in newStyle) {
    cur = newStyle[name]
    if(cur ! == oldStyle[name]) {// ie9 setting to null has no effect, must use empty string
      setProp(el, name, cur == null ? ' ' : cur)
    }
  }
}
Copy the code

To set a STYLE for a DOM, there are setProperty and style[property] methods. When we write components, there are many ways to write them. Array objects, strings, objects are supported! There is a framework design concept involved here: the design of the application layer. When designing a tool or component, design it from the top down — think about how to use it first, that is, how to design the application layer first, and then how to connect it to the underlying layer (style.setProperty and style.csspropertyName in 🌰). Starting from the bottom (fixed, limited features) tends to limit the imagination and thus reduce the ease of use of the framework. (❗❗❗ Tips)

summary

This section analyzes the patch process of the component: Unlike normal nodes, the component returns true when it executes createComponent on createElm. In createComponent, _init is re-executed, and then the component’s mount logic is executed (mount -> mountComponent -> updateComponent -> vm._render-> vm._update).

conclusion

Finally, summarize this article with a picture:

After VNode is obtained through vm._render, vm._update is executed to start rendering. Different clients have different patches. The difference is smoothed at one time by using the Curlization technique in the createPatchFunction. For normal elements, an element is created, and then invokeCreateHooks are executed to handle attributes like style, class, attrs, and so on. This part involves the framework design idea – layering, from the application layer, to make the framework easier to use. Finally, remove the placeholder node and insert the hook. For component nodes, createComponent will return true when executed to createElm, and will execute init hooks that were installed when the component VNode was created, thus executing the component’s constructor. Init -> $mount -> mountComponent -> updateComponent -> vm._render -> vm._update stream

When the data changes, a VNode update is triggered, and the VNode diff is executed and re-rendered, which will be analyzed in the next article. Pay attention to the code farmer xiaoyu, work together, harvest together!