Vue Implementation 01

Constructor of Vue

We know, when we are using Vue, we need to use the new operator to invoke it. It demonstrates that Vue should be a constructor. So what we need to do first is: to make clear of the constructor of Vue.

The Prototype of Vue Constructor

In Getting into the project of Vue, we mentioned we will dive in it by npm run dev.

"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",

When we run npm run dev, according to the configuration file in scripts/config.js:

  // Runtime+compiler development build (Browser)
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  }

It shows that the entry file is web/entry-runtime-with-compiler.js, and finally products dist/vue.js. It is a umd module. Therefore, let's begin with entry file, and find Vue's constructor and make it clearer.

But now there is a question: in web/entry-runtime-with-compiler.js, which directory is web indicated? It is a alias setting, we can open scripts/alias.js:

const path = require('path')

const resolve = p => path.resolve(__dirname, '../', p)

module.exports = {
  vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
  compiler: resolve('src/compiler'),
  core: resolve('src/core'),
  shared: resolve('src/shared'),
  web: resolve('src/platforms/web'),
  weex: resolve('src/platforms/weex'),
  server: resolve('src/server'),
  entries: resolve('src/entries'),
  sfc: resolve('src/sfc')
}

There is a line:

web: resolve('src/platforms/web')

So web points to src/platforms/web. Along with that, alias.js includes more other aliases, so we can refer to this file to find the according directory without other description.

Then we can get into the topic. Open src/platforms/web/entry-runtime-with-compiler.js, we can see the following line:

import Vue from './runtime/index'

It shows that it is not the source of the constructor of Vue, and the Vue of this file is introduced in ./runtime/index. So we open the runtime folder in current directory, and we can see in index.js:

import Vue from 'core/index'

Same, it indicated that runtime/index.js is not the source of the constructor of Vue neither. You should open core/index.js as well. In the configuration of scripts/alias.js, core pointed to src/core. We can see in src/core/index.js:

import Vue from './instance/index'

By the same method, open ./instance/index.js:

// import five functions from five different files (excluding warn)
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

// define the constructor of Vue
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)
}

// pass Vue as parameter to five functions
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

// export Vue
export default Vue

We can see, this is the souce of the constructor of Vue. The code above is all in ./instance/index.js and it is relatively short.

  1. First, it imports five functions from init.js, state.js, render.js, events.js and lifecycle.js, which is initMixin, stateMixin, renderMixin, eventsMixin and lifecycleMixin.
  2. Second, it defines the constructor of Vue, and it checks that if you are in production mode, you can only use the new operator to invoke Vue.
  3. Finally, it passes Vue as parameter info the imported functions and export Vue.

So what do these functions do? Let's deep into initMixin first. Open ./init.js and find initMixin, we can see:

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // ... body of _init, omitted.
  }
}

This function aimed to add _init function to the prototype of Vue. The _init function seems like a internal initialization function. Actually, we met this function once at instance/index.js:

// define Vue constructor
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')
  }
  // over here
  this._init(options)
}

In the Vue's constructor, there is a line: this._init(options). This shows that when we invoke new Vue(), this._init(option) will be invoked.

Then we open ./state.js and look into stateMixin. At the beginning of this function, there is a part of code:

  const dataDef = {}
  dataDef.get = function () { return this._data }
  const propsDef = {}
  propsDef.get = function () { return this._props }
  if (process.env.NODE_ENV !== 'production') {
    dataDef.set = function (newData: Object) {
      warn(
        'Avoid replacing instance root $data. ' +
        'Use nested data properties instead.',
        this
      )
    }
    propsDef.set = function () {
      warn(`$props is readonly.`, this)
    }
  }
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)

Let's dive into last two lines first. It uses Object.defineProperty to define two properties in Vue.prototype, which is our familiar $data and $props. The definition of these properties is written in dataDef and propsDef. Let's go further and look at the definition of these two objects. First is get:

const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }

We can see, what $data proxing is the instance property of _data, and what $props proxing is the instance property of _props. And then there is a check of it is in production environment. If it's not in production, it will define set for $data and $props. Actually, it just used to warn you that you should not mutate it directly.

That is, $data and $props are both readonly properties. So, if you need to implement a readonly property in Javascript, you know how to do now.

Then stateMixin define three functions in Vue.prototype:

  Vue.prototype.$set = set
  Vue.prototype.$delete = del

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
   // ...
  }

$set, $delete and $watch is what you already have known about at Vue's instance properties page.

Next is eventsMixin. This function is defined in ./events.js. We can open this file and find eventsMixin. This function add four functions to Vue.prototype, which is:

Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {}
Vue.prototype.$once = function (event: string, fn: Function): Component {}
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {}
Vue.prototype.$emit = function (event: string): Component {}

Lastly, the renderMixin function. It is included in render.js. This function invoked installRenderHelpers function with Vue.prototype as parameter. This function is introduced in render.js which is located in render-helpers/index.js. Open this file and find installRenderHelpers function:

export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
}

All above is the code of installRenderHelpers. We can see, what this function do is to add a set of functions to Vue.prototype. We don't need to make clear of what do these functions do and we will talk about these.

After execution of installRenderHelpers, renderMixin added two more functions to Vue.prototype, which is $nextTick and _render. Finally, Vue.prototype is attached by these functions after renderMixin:

// in installRenderHelpers
Vue.prototype._o = markOnce
Vue.prototype._n = toNumber
Vue.prototype._s = toString
Vue.prototype._l = renderList
Vue.prototype._t = renderSlot
Vue.prototype._q = looseEqual
Vue.prototype._i = looseIndexOf
Vue.prototype._m = renderStatic
Vue.prototype._f = resolveFilter
Vue.prototype._k = checkKeyCodes
Vue.prototype._b = bindObjectProps
Vue.prototype._v = createTextVNode
Vue.prototype._e = createEmptyVNode
Vue.prototype._u = resolveScopedSlots
Vue.prototype._g = bindObjectListeners

Vue.prototype.$nextTick = function (fn: Function) {}
Vue.prototype._render = function (): VNode {}

Now, instance/index.js finished its run in npm run dev. We know that every *Mixin is used to decorate Vue.prototype and mount some properties and functions to it. Next, we need to do a very important thingis to combine all of content above into a seperate place for searching. We made a appendix here: // TODO. When we talk about it later, we can find that function fast and keep it clear.