Frontend Development 16 min read

Analyzing Vue.js vm._update and __patch__ Implementation

This article provides a detailed walkthrough of Vue.js’s internal vm._update method, its reliance on the core __patch__ function, and the surrounding lifecycle mechanisms, illustrating each step with source code excerpts and diagrams to clarify how virtual DOM nodes are transformed into real DOM elements.

Xueersi Online School Tech Team
Xueersi Online School Tech Team
Xueersi Online School Tech Team
Analyzing Vue.js vm._update and __patch__ Implementation

In previous posts we examined Vue's initialization steps, focusing on virtual DOM nodes and component nodes that eventually become arguments to vm._update . This article dives into the inner workings of vm._update , starting with its core helper __patch__ , which resides in the same file as mountComponent .

Opening core/instance/lifecycle.js reveals the lifecycleMixin where Vue.prototype._update is defined:

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const restoreActiveInstance = setActiveInstance(vm)
  vm._vnode = vnode
  if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  // ... (omitted code)
}

The actual patching work is delegated to __patch__ , which is assigned in platform/web/runtime/index.js :

import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop

The patch function is created by createPatchFunction in core/vdom/patch :

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })

createPatchFunction returns a higher‑order patch function that handles vnode comparison, creation, and removal. To see the flow in action, consider the following demo:

import Vue from ‘vue’
import app from ‘@app’

Vue.config.productionTip = false
console.log(app)

new Vue({
  el: ‘#root’,
  render: h => {
    const vnode = h(app)
    console.log(vnode)
    return vnode
  }
})

// app.vue

When the demo runs, the patch function receives four arguments; the most important are oldVnode (the real DOM element #root ) and vnode (the component vnode generated by the render function). Because this is not server‑side rendering, hydrating is false and removeOnly is also false .

The first step converts the real element into an empty vnode:

function emptyNodeAt (elm) {
  return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}

Next, createElm builds the real DOM tree from the vnode. The function contains many branches for components, elements, comments, and text nodes. Below is a trimmed version that shows the main flow for element nodes:

function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    vnode = ownerArray[index] = cloneVNode(vnode)
  }
  vnode.isRootInsert = !nested
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode)
    createChildren(vnode, children, insertedVnodeQueue)
    if (isDef(data)) {
      invokeCreateHooks(vnode, insertedVnodeQueue)
    }
    insert(parentElm, vnode.elm, refElm)
  } else if (isTrue(vnode.isComment)) {
    vnode.elm = nodeOps.createComment(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  } else {
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
}

The insert helper decides whether to use insertBefore or appendChild :

function insert (parent, elm, ref) {
  if (isDef(parent)) {
    if (isDef(ref)) {
      if (nodeOps.parentNode(ref) === parent) {
        nodeOps.insertBefore(parent, elm, ref)
      }
    } else {
      nodeOps.appendChild(parent, elm)
    }
  }
}

When the vnode represents a component, createComponent is invoked. Its core logic creates the component instance and mounts it:

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */)
    }
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

The component’s init hook eventually calls createComponentInstanceForVnode to instantiate the child Vue instance:

function createComponentInstanceForVnode (vnode, parent) {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  return new vnode.componentOptions.Ctor(options)
}

Two important properties are established: _parentVnode (the placeholder vnode for the component) and parent (the Vue instance that created the component). This hierarchy is visualised in the article’s diagrams.

After the instance is created, initInternalComponent merges component options:

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  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
  }
}

The lifecycle mixin also sets up parent‑child relationships:

parent.$children.push(vm)
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []

Finally, mountComponent creates a render watcher that calls vm._update(vm._render(), hydrating) on each reactive change:

export function mountComponent (vm: Component, el: ?Element, hydrating?: boolean) {
  vm.$el = el
  // ...
  const updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
}

The render function of app.vue produces the following virtual DOM, which __patch__ turns into real DOM:

<div id="root" class="app">
  <p>{{ msg }}</p>
</div>

With the component hierarchy and patching process fully traced, the article concludes that understanding vm._update and __patch__ is essential for mastering Vue’s reactivity and rendering pipeline.

Frontend DevelopmentVue.jsVirtual DOMComponent LifecyclePatch Function
Xueersi Online School Tech Team
Written by

Xueersi Online School Tech Team

The Xueersi Online School Tech Team, dedicated to innovating and promoting internet education technology.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.