Analyzing Vue.js Instance Initialization: How new Vue Works Internally
This article walks through Vue's source code to explain how the new Vue constructor initializes a component, merges options, proxies data, mounts the instance, and ultimately replaces template interpolations with reactive data during the rendering process.
In this tutorial the author revisits the Vue source code to demystify what happens when new Vue({ ... }) is executed. The article starts with a simple demo component and poses three questions about template interpolation, property access via this.xxx , and the steps performed before the DOM is updated.
The entry point of Vue is src/core/instance/index.js , where the constructor merely calls this._init(options) . The _init method is mixed into the prototype by initMixin and performs a series of initialization steps:
import { initMixin } from './init'
function Vue (options) {
if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue)
export default VueInside init.js , Vue.prototype._init assigns a unique _uid , merges options, sets up proxies, lifecycle hooks, events, rendering helpers, and finally calls vm.$mount(vm.$options.el) if an element is specified.
Vue.prototype._init = function (options) {
const vm = this
vm._uid = uid++
vm._isVue = true
// merge options
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm)
}
// dev‑only proxy
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}The initState function (in core/instance/state.js ) initializes props , methods , data , computed , and watch . For data it calls initData , which proxies each data property onto the Vue instance using Object.defineProperty :
function initData (vm) {
let data = vm.$options.data
data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (methods && hasOwn(methods, key)) {
warn(`Method "${key}" has already been defined as a data property.`, vm)
}
if (props && hasOwn(props, key)) {
warn(`The data property "${key}" is already declared as a prop. Use prop default value instead.`, vm)
} else if (!isReserved(key)) {
proxy(vm, '_data', key)
}
}
observe(data, true /* asRootData */)
}
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}When vm.$mount is called (defined in src/platforms/runtime/index.js ), it simply resolves the element and delegates to mountComponent :
Vue.prototype.$mount = function (el, hydrating) {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}mountComponent (in core/instance/lifecycle.js ) creates a render watcher that invokes updateComponent , which finally calls vm._update(vm._render(), hydrating) . This is where the template interpolation (e.g., {{ message }} ) is replaced with the actual data value.
function mountComponent (vm, el, hydrating) {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') || vm.$options.el || el) {
warn('You are using the runtime-only build of Vue where the template compiler is not available...', vm)
} else {
warn('Failed to mount component: template or render function not defined.', vm)
}
}
}
callHook(vm, 'beforeMount')
let updateComponent
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
// performance tracking omitted
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}The article also highlights the difference between the runtime‑only and runtime‑+‑compiler builds of Vue, explains why using a template with the runtime‑only build triggers a warning, and points out that the ultimate goal of all these steps is to compile the template into a render function that produces a virtual DOM.
By the end of the piece the reader understands how Vue initializes a component, proxies data, sets up reactivity, mounts the instance, and begins the render‑watcher cycle that drives DOM updates.
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.