Frontend Development 14 min read

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.

Xueersi Online School Tech Team
Xueersi Online School Tech Team
Xueersi Online School Tech Team
Analyzing Vue.js Instance Initialization: How new Vue Works Internally

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 Vue

Inside 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.

frontendJavaScriptVueframeworkInitialization
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.