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.
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 : noopThe 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.vueWhen 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.
Xueersi Online School Tech Team
The Xueersi Online School Tech Team, dedicated to innovating and promoting internet education technology.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.